Repository: veighna-global/vnpy_binance Branch: main Commit: 3408672b12bf Files: 16 Total size: 236.6 KB Directory structure: gitextract_nwvrw0w8/ ├── .github/ │ ├── CODE_OF_CONDUCT.md │ ├── ISSUE_TEMPLATE.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── SUPPORT.md │ └── workflows/ │ └── pythonapp.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── pyproject.toml ├── script/ │ └── run.py └── vnpy_binance/ ├── __init__.py ├── inverse_gateway.py ├── linear_gateway.py ├── portfolio_gateway.py └── spot_gateway.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/CODE_OF_CONDUCT.md ================================================ # 行为准则 这是一份VeighNa项目社区的行为准则,也是项目作者自己在刚入行量化金融行业时对于理想中的社区的期望: * 为交易员而生:作为一款从金融机构量化业务中诞生的交易系统开发框架,设计上都优先满足机构专业交易员的使用习惯,而不是其他用户(散户、爱好者、技术人员等) * 对新用户友好,保持耐心:大部分人在接触新东西的时候都是磕磕碰碰、有很多的问题,请记住此时别人对你伸出的援助之手,并把它传递给未来需要的人 * 尊重他人,慎重言行:礼貌文明的交流方式除了能得到别人同样的回应,更能减少不必要的摩擦,保证高效的交流 ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ ## 环境 * 操作系统: 如Windows 11或者Ubuntu 22.04 * Python版本: 如VeighNa Studio-4.0.0 * VeighNa版本: 如v4.0.0发行版或者dev branch 20250320(下载日期) ## Issue类型 三选一:Bug/Enhancement/Question ## 预期程序行为 ## 实际程序行为 ## 重现步骤 针对Bug类型Issue,请提供具体重现步骤以及报错截图 ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ 建议每次发起的PR内容尽可能精简,复杂的修改请拆分为多次PR,便于管理合并。 ## 改进内容 1. 2. 3. ## 相关的Issue号(如有) Close # ================================================ FILE: .github/SUPPORT.md ================================================ # 获取帮助 在开发和使用项目的过程中遇到问题时,获取帮助的渠道包括: * Github Issues:[Issues页面](https://github.com/veighna-global/vnpy_evo/issues) ================================================ FILE: .github/workflows/pythonapp.yml ================================================ name: Python application on: [push] jobs: build: runs-on: windows-latest steps: - uses: actions/checkout@v1 - name: Set up Python 3.13 uses: actions/setup-python@v1 with: python-version: '3.13' - name: Install dependencies run: | python -m pip install --upgrade pip pip install ta-lib==0.6.3 --index=https://pypi.vnpy.com pip install vnpy ruff mypy uv - name: Lint with ruff run: | # Run ruff linter based on pyproject.toml configuration ruff check . - name: Type check with mypy run: | # Run mypy type checking based on pyproject.toml configuration mypy vnpy_binance - name: Build packages with uv run: | # Build source distribution and wheel distribution uv build ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ *.pya ================================================ FILE: CHANGELOG.md ================================================ # 2026.04.27 1. update linear and portfolio gateways with new endpoints for routed streams # 2026.04.15 1. fix missing data when querying bar history of inverse contract # 2026.04.13 1. update tick.datetime when receiving book update 2. fix bug: start time is greater than end time when querying bar history # 2026.03.27 1. add TRADIFI_PERPETUAL contract support for portfolio gateway # 2026.03.26 1. support TRADIFI_PERPETUAL contract # 2026.03.06 1. new PortfolioGateway for portfolio margin mode 2. update SpotGateway to latest version 3. support funding rate subscription for linear swap # 2026.01.25 1. use periodic subscription mechanism to avoid connection loss # 2025.06.17 1. refactor BinanceSpotGateway and BinanceInverseGateway 2. remove unused disconnect function # 2025.05.08 1. remove dependency on vnpy_evo 2. change to use GLOBAL exchange 3. refactor BinanceLinearGateway # 2025.1.25 1. BinanceLinearGateway replace REST API with Websocket API for sending orders # 2024.12.16 1. write log (event) when exception raised by websocket client # 2024.9.16 1. add more detail to TickData.extra including: active_volume/active_turnover/trade_count 2. BinanceLinearGateway upgrade to v3 api for querying account and position # 2024.9.4 1. add extra data dict for BarData # 2024.9.3 1. use vnpy_evo for rest and websocket client # 2024.8.10 1. fix the problem of datetime timezone 2. fix the problem of account data receiving no update when the balance is all sold out 3. only keep user stream when key is provided # 2024.5.7 1. use numpy.format_float_positional to improve float number precisio… 2. output log message of time offse 3. query private data after time offset is calculated ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2015-present, Xiaoyou Chen 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. ================================================ FILE: README.md ================================================ # Binance trading gateway for VeighNa

## Introduction This gateway is developed based on Binance's REST and Websocket API, and supports spot, linear contract and inverse contract trading. **For derivatives contract trading, please notice:** 1. Only supports cross margin mode. 2. Only supports one-way position mode. ## Install Users can easily install ``vnpy_binance`` by pip according to the following command. ``` pip install vnpy_binance ``` Also, users can install ``vnpy_binance`` using the source code. Clone the repository and install as follows: ``` git clone https://github.com/veighna-global/vnpy_binance.git && cd vnpy_binance python setup.py install ``` ## A Simple Example Save this as run.py. ``` from vnpy.event import EventEngine from vnpy.trader.engine import MainEngine from vnpy.trader.ui import MainWindow, create_qapp from vnpy_binance import ( BinanceLinearGateway, ) def main() -> None: """main entry""" qapp = create_qapp() event_engine = EventEngine() main_engine = MainEngine(event_engine) main_engine.add_gateway(BinanceLinearGateway) main_window = MainWindow(main_engine, event_engine) main_window.showMaximized() qapp.exec() if __name__ == "__main__": main() ``` ================================================ FILE: pyproject.toml ================================================ [project] name = "vnpy_binance" dynamic = ["version"] description = "BINANCE trading gateway for VeighNa." readme = "README.md" license = {text = "MIT"} authors = [{name = "VeighNa Global", email = "veighna@hotmail.com"}] classifiers = [ "Development Status :: 5 - Production/Stable", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX :: Linux", "Operating System :: MacOS", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Office/Business :: Financial :: Investment", "Programming Language :: Python :: Implementation :: CPython", "License :: OSI Approved :: MIT License", "Natural Language :: English", ] requires-python = ">=3.10" keywords = ["quant", "quantitative", "investment", "trading", "algotrading", "binance", "btc", "crypto"] dependencies = [ "vnpy_rest", "vnpy_websocket", ] [project.urls] "Homepage" = "https://www.github.com/veighna-global" "Source" = "https://www.github.com/veighna-global" [build-system] requires = ["hatchling>=1.27.0"] build-backend = "hatchling.build" [tool.hatch.version] path = "vnpy_binance/__init__.py" pattern = "__version__ = ['\"](?P[^'\"]+)['\"]" [tool.hatch.build.targets.wheel] packages = ["vnpy_binance"] include-package-data = true [tool.hatch.build.targets.sdist] include = ["vnpy_binance*"] [tool.ruff] target-version = "py310" output-format = "full" [tool.ruff.lint] select = [ "B", # flake8-bugbear "E", # pycodestyle error "F", # pyflakes "UP", # pyupgrade "W", # pycodestyle warning ] ignore = ["E501"] [tool.mypy] python_version = "3.10" warn_return_any = true warn_unused_configs = true disallow_untyped_defs = true disallow_incomplete_defs = true check_untyped_defs = true disallow_untyped_decorators = true no_implicit_optional = true strict_optional = true warn_redundant_casts = true warn_unused_ignores = true warn_no_return = true ignore_missing_imports = true [[tool.mypy.overrides]] module = [ "vnpy.*", "vnpy_rest.*", "vnpy_websocket.*" ] ignore_errors = true ignore_missing_imports = true ================================================ FILE: script/run.py ================================================ from vnpy.event import EventEngine from vnpy.trader.engine import MainEngine from vnpy.trader.ui import MainWindow, create_qapp from vnpy_binance import ( BinanceLinearGateway, BinanceInverseGateway, BinanceSpotGateway, BinancePortfolioGateway ) from vnpy_datamanager import DataManagerApp def main() -> None: """main entry""" qapp = create_qapp() event_engine = EventEngine() main_engine = MainEngine(event_engine) main_engine.add_gateway(BinanceLinearGateway) main_engine.add_gateway(BinanceInverseGateway) main_engine.add_gateway(BinanceSpotGateway) main_engine.add_gateway(BinancePortfolioGateway) main_engine.add_app(DataManagerApp) main_window = MainWindow(main_engine, event_engine) main_window.showMaximized() qapp.exec() if __name__ == "__main__": main() ================================================ FILE: vnpy_binance/__init__.py ================================================ # The MIT License (MIT) # # Copyright (c) 2015-present, Xiaoyou Chen # # 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 .linear_gateway import BinanceLinearGateway from .inverse_gateway import BinanceInverseGateway from .spot_gateway import BinanceSpotGateway from .portfolio_gateway import BinancePortfolioGateway __version__ = "2026.04.27" __all__ = [ "BinanceLinearGateway", "BinanceInverseGateway", "BinanceSpotGateway", "BinancePortfolioGateway" ] ================================================ FILE: vnpy_binance/inverse_gateway.py ================================================ import hashlib import hmac import time import urllib.parse from copy import copy from typing import Any from collections.abc import Callable from time import sleep from datetime import datetime, timedelta from numpy import format_float_positional from vnpy.event import Event, EventEngine from vnpy.trader.constant import ( Direction, Exchange, Product, Status, OrderType, Interval ) from vnpy.trader.gateway import BaseGateway from vnpy.trader.object import ( TickData, OrderData, TradeData, AccountData, ContractData, PositionData, BarData, OrderRequest, CancelRequest, SubscribeRequest, HistoryRequest ) from vnpy.trader.event import EVENT_TIMER from vnpy.trader.utility import round_to, ZoneInfo from vnpy_rest import Request, RestClient, Response from vnpy_websocket import WebsocketClient # Timezone constant UTC_TZ = ZoneInfo("UTC") # Real server hosts REAL_REST_HOST: str = "https://dapi.binance.com" REAL_TRADE_HOST: str = "wss://ws-dapi.binance.com/ws-dapi/v1" REAL_USER_HOST: str = "wss://dstream.binance.com/ws/" REAL_DATA_HOST: str = "wss://dstream.binance.com/stream" # Testnet server hosts TESTNET_REST_HOST: str = "https://testnet.binancefuture.com" TESTNET_TRADE_HOST: str = "wss://testnet.binancefuture.com/ws-dapi/v1" TESTNET_USER_HOST: str = "wss://dstream.binancefuture.com/ws/" TESTNET_DATA_HOST: str = "wss://dstream.binancefuture.com/stream" # Order status map STATUS_BINANCE2VT: dict[str, Status] = { "NEW": Status.NOTTRADED, "PARTIALLY_FILLED": Status.PARTTRADED, "FILLED": Status.ALLTRADED, "CANCELED": Status.CANCELLED, "REJECTED": Status.REJECTED, "EXPIRED": Status.CANCELLED } # Order type map ORDERTYPE_VT2BINANCE: dict[OrderType, tuple[str, str]] = { OrderType.LIMIT: ("LIMIT", "GTC"), OrderType.MARKET: ("MARKET", "GTC"), OrderType.FAK: ("LIMIT", "IOC"), OrderType.FOK: ("LIMIT", "FOK"), } ORDERTYPE_BINANCE2VT: dict[tuple[str, str], OrderType] = {v: k for k, v in ORDERTYPE_VT2BINANCE.items()} # Direction map DIRECTION_VT2BINANCE: dict[Direction, str] = { Direction.LONG: "BUY", Direction.SHORT: "SELL" } DIRECTION_BINANCE2VT: dict[str, Direction] = {v: k for k, v in DIRECTION_VT2BINANCE.items()} # Product map PRODUCT_BINANCE2VT: dict[str, Product] = { "PERPETUAL": Product.SWAP, "PERPETUAL_DELIVERING": Product.SWAP, "CURRENT_MONTH": Product.FUTURES, "NEXT_MONTH": Product.FUTURES, "CURRENT_QUARTER": Product.FUTURES, "NEXT_QUARTER": Product.FUTURES, } # Kline interval map INTERVAL_VT2BINANCE: dict[Interval, str] = { Interval.MINUTE: "1m", Interval.HOUR: "1h", Interval.DAILY: "1d", } # Timedelta map TIMEDELTA_MAP: dict[Interval, timedelta] = { Interval.MINUTE: timedelta(minutes=1), Interval.HOUR: timedelta(hours=1), Interval.DAILY: timedelta(days=1), } # Set weboscket timeout to 24 hour WEBSOCKET_TIMEOUT = 24 * 60 * 60 class BinanceInverseGateway(BaseGateway): """ The Binance inverse trading gateway for VeighNa. This gateway provides trading functionality for Binance Coin-M perpetual contracts and delivery futures through their API. Features: 1. Only support crossed position 2. Only support one-way mode 3. Provides market data, trading, and account management capabilities """ default_name: str = "BINANCE_INVERSE" default_setting: dict = { "API Key": "", "API Secret": "", "Server": ["REAL", "TESTNET"], "Kline Stream": ["False", "True"], "Proxy Host": "", "Proxy Port": 0 } exchanges: list[Exchange] = [Exchange.GLOBAL] def __init__(self, event_engine: EventEngine, gateway_name: str) -> None: """ The init method of the gateway. This method initializes the gateway components including REST API, trading API, user data API, and market data API. It also sets up the data structures for order and contract storage. Parameters: event_engine: the global event engine object of VeighNa gateway_name: the unique name for identifying the gateway """ super().__init__(event_engine, gateway_name) self.trade_api: TradeApi = TradeApi(self) self.user_api: UserApi = UserApi(self) self.md_api: MdApi = MdApi(self) self.rest_api: RestApi = RestApi(self) self.orders: dict[str, OrderData] = {} self.symbol_contract_map: dict[str, ContractData] = {} self.name_contract_map: dict[str, ContractData] = {} def connect(self, setting: dict) -> None: """ Start server connections. This method establishes connections to Binance servers using the provided settings. Parameters: setting: A dictionary containing connection parameters including API credentials, server selection, and proxy configuration """ key: str = setting["API Key"] secret: str = setting["API Secret"] server: str = setting["Server"] kline_stream: bool = setting["Kline Stream"] == "True" proxy_host: str = setting["Proxy Host"] proxy_port: int = setting["Proxy Port"] self.rest_api.connect(key, secret, server, proxy_host, proxy_port) self.trade_api.connect(key, secret, server, proxy_host, proxy_port) self.md_api.connect(server, kline_stream, proxy_host, proxy_port) self.event_engine.register(EVENT_TIMER, self.process_timer_event) def subscribe(self, req: SubscribeRequest) -> None: """ Subscribe to market data. This method forwards the subscription request to the market data API. Parameters: req: Subscription request object containing the symbol to subscribe """ self.md_api.subscribe(req) def send_order(self, req: OrderRequest) -> str: """ Send new order. This method forwards the order request to the trading API. Parameters: req: Order request object containing order details Returns: str: The VeighNa order ID if successful, empty string if failed """ return self.trade_api.send_order(req) def cancel_order(self, req: CancelRequest) -> None: """ Cancel existing order. This method forwards the cancellation request to the trading API. Parameters: req: Cancel request object containing order details """ self.trade_api.cancel_order(req) def query_account(self) -> None: """ Query account balance. Not required since Binance provides websocket updates for account balances. """ pass def query_position(self) -> None: """ Query current positions. Not required since Binance provides websocket updates for positions. """ pass def query_history(self, req: HistoryRequest) -> list[BarData]: """ Query historical kline data. This method forwards the history request to the REST API. Parameters: req: History request object containing query parameters Returns: list[BarData]: List of historical kline data bars """ return self.rest_api.query_history(req) def close(self) -> None: """ Close server connections. This method stops all API connections and releases resources. """ self.rest_api.stop() self.user_api.stop() self.md_api.stop() self.trade_api.stop() def process_timer_event(self, event: Event) -> None: """ Process timer task. This function is called regularly by the event engine to perform scheduled tasks, such as keeping the user stream alive. Parameters: event: Timer event object """ self.rest_api.keep_user_stream() self.md_api.subscribe_new_channels() def on_order(self, order: OrderData) -> None: """ Save a copy of order and then push to event engine. Parameters: order: Order data object """ self.orders[order.orderid] = copy(order) super().on_order(order) def get_order(self, orderid: str) -> OrderData | None: """ Get previously saved order by order id. Parameters: orderid: The ID of the order to retrieve Returns: Order data object if found, None otherwise """ return self.orders.get(orderid, None) def on_contract(self, contract: ContractData) -> None: """ Save contract data in mappings and push to event engine. Parameters: contract: Contract data object """ self.symbol_contract_map[contract.symbol] = contract self.name_contract_map[contract.name] = contract super().on_contract(contract) def get_contract_by_symbol(self, symbol: str) -> ContractData | None: """ Get contract data by VeighNa symbol. Parameters: symbol: VeighNa symbol (e.g. "BTC_SWAP_BINANCE") Returns: Contract data object if found, None otherwise """ return self.symbol_contract_map.get(symbol, None) def get_contract_by_name(self, name: str) -> ContractData | None: """ Get contract data by exchange symbol name. Parameters: name: Exchange symbol name (e.g. "BTCUSD") Returns: Contract data object if found, None otherwise """ return self.name_contract_map.get(name, None) class RestApi(RestClient): """ The REST API of BinanceInverseGateway. This class handles HTTP requests to Binance API endpoints, including: - Authentication and signature generation - Contract information queries - Account and position queries - Order management - Historical data queries - User data stream management """ def __init__(self, gateway: BinanceInverseGateway) -> None: """ The init method of the API. This method initializes the REST API with a reference to the parent gateway. Parameters: gateway: the parent gateway object for pushing callback data """ super().__init__() self.gateway: BinanceInverseGateway = gateway self.gateway_name: str = gateway.gateway_name self.user_api: UserApi = self.gateway.user_api self.key: str = "" self.secret: bytes = b"" self.user_stream_key: str = "" self.keep_alive_count: int = 0 self.time_offset: int = 0 self.order_count: int = 1_000_000 self.order_prefix: str = "" def sign(self, request: Request) -> Request: """ Standard callback for signing a request. This method adds the necessary authentication parameters and signature to requests that require API key authentication. It handles: 1. Path construction with query parameters 2. Timestamp generation with server time offset adjustment 3. HMAC-SHA256 signature generation 4. Required authentication headers Parameters: request: Request object to be signed Returns: Request: Modified request with authentication parameters """ # Construct path with query parameters if they exist if request.params: path: str = request.path + "?" + urllib.parse.urlencode(request.params) else: request.params = {} path = request.path # Get current timestamp in milliseconds timestamp: int = int(time.time() * 1000) # Adjust timestamp based on time offset with server if self.time_offset > 0: timestamp -= abs(self.time_offset) elif self.time_offset < 0: timestamp += abs(self.time_offset) # Add timestamp to request parameters request.params["timestamp"] = timestamp # Generate signature using HMAC SHA256 query: str = urllib.parse.urlencode(sorted(request.params.items())) signature: str = hmac.new( self.secret, query.encode("utf-8"), hashlib.sha256 ).hexdigest() # Append signature to query string query += f"&signature={signature}" path = request.path + "?" + query # Update request with signed path and clear params/data request.path = path request.params = {} request.data = {} # Add required headers for API authentication request.headers = { "Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json", "X-MBX-APIKEY": self.key, "Connection": "close" } return request def connect( self, key: str, secret: str, server: str, proxy_host: str, proxy_port: int ) -> None: """Start server connection""" self.key = key self.secret = secret.encode() self.proxy_port = proxy_port self.proxy_host = proxy_host self.server = server self.order_prefix = datetime.now().strftime("%y%m%d%H%M%S") if self.server == "REAL": self.init(REAL_REST_HOST, proxy_host, proxy_port) else: self.init(TESTNET_REST_HOST, proxy_host, proxy_port) self.start() self.gateway.write_log("REST API started") self.query_time() def query_time(self) -> None: """ Query server time to calculate local time offset. This function sends a request to get the exchange server time, which is used to calculate the local time offset for timestamp synchronization. """ path: str = "/dapi/v1/time" self.add_request( "GET", path, callback=self.on_query_time ) def query_account(self) -> None: """ Query account balance. This function sends a request to get the account balance information, including wallet balance, available balance, and margin. """ path: str = "/dapi/v1/account" self.add_request( method="GET", path=path, callback=self.on_query_account, ) def query_position(self) -> None: """ Query holding positions. This function sends a request to get current position data, including position amount, entry price, and unrealized profit/loss. """ path: str = "/dapi/v1/positionRisk" self.add_request( method="GET", path=path, callback=self.on_query_position, ) def query_order(self) -> None: """ Query open orders. This function sends a request to get all active orders that have not been fully filled or cancelled. """ path: str = "/dapi/v1/openOrders" self.add_request( method="GET", path=path, callback=self.on_query_order, ) def query_contract(self) -> None: """ Query available contracts. This function sends a request to get exchange information, including all available trading instruments, their precision, and trading rules. """ path: str = "/dapi/v1/exchangeInfo" self.add_request( method="GET", path=path, callback=self.on_query_contract, ) def start_user_stream(self) -> None: """ Create listen key for user stream. This function sends a request to create a listen key which is required to establish a user data websocket connection. """ path: str = "/dapi/v1/listenKey" self.add_request( method="POST", path=path, callback=self.on_start_user_stream, ) def keep_user_stream(self) -> None: """ Extend listen key validity. This function sends a request to keep the listen key active, which is required to maintain the user data websocket connection. The listen key will expire after 60 minutes if not refreshed. """ if not self.user_stream_key: return self.keep_alive_count += 1 if self.keep_alive_count < 600: return self.keep_alive_count = 0 params: dict = {"listenKey": self.user_stream_key} path: str = "/dapi/v1/listenKey" self.add_request( method="PUT", path=path, callback=self.on_keep_user_stream, params=params, on_error=self.on_keep_user_stream_error ) def on_query_time(self, data: dict, request: Request) -> None: """ Callback of server time query. This function processes the server time response and calculates the time offset between local and server time, which is used for request timestamp synchronization. Parameters: data: Response data from the server request: Original request object """ local_time: int = int(time.time() * 1000) server_time: int = int(data["serverTime"]) self.time_offset = local_time - server_time self.gateway.write_log(f"Server time updated, local offset: {self.time_offset}ms") self.query_contract() def on_query_account(self, data: dict, request: Request) -> None: """ Callback of account balance query. This function processes the account balance response and creates AccountData objects for each asset in the account. Parameters: data: Response data from the server request: Original request object """ for asset in data["assets"]: account: AccountData = AccountData( accountid=asset["asset"], balance=float(asset["walletBalance"]), frozen=float(asset["maintMargin"]), gateway_name=self.gateway_name ) if account.balance: self.gateway.on_account(account) self.gateway.write_log("Account data received") def on_query_position(self, data: list, request: Request) -> None: """ Callback of holding positions query. This function processes the position data response and creates PositionData objects for each position held. Parameters: data: Response data from the server request: Original request object """ for d in data: name: str = d["symbol"] contract: ContractData | None = self.gateway.get_contract_by_name(name) if not contract: continue position: PositionData = PositionData( symbol=contract.symbol, exchange=Exchange.GLOBAL, direction=Direction.NET, volume=float(d["positionAmt"]), price=float(d["entryPrice"]), pnl=float(d["unRealizedProfit"]), gateway_name=self.gateway_name, ) if position.volume: self.gateway.on_position(position) self.gateway.write_log("Position data received") def on_query_order(self, data: list, request: Request) -> None: """ Callback of open orders query. This function processes the open orders response and creates OrderData objects for each active order. Parameters: data: Response data from the server request: Original request object """ for d in data: key: tuple[str, str] = (d["type"], d["timeInForce"]) order_type: OrderType | None = ORDERTYPE_BINANCE2VT.get(key, None) if not order_type: continue contract: ContractData | None = self.gateway.get_contract_by_symbol(d["symbol"]) if not contract: continue order: OrderData = OrderData( orderid=d["clientOrderId"], symbol=contract.symbol, exchange=Exchange.GLOBAL, price=float(d["price"]), volume=float(d["origQty"]), type=order_type, direction=DIRECTION_BINANCE2VT[d["side"]], traded=float(d["executedQty"]), status=STATUS_BINANCE2VT[d["status"]], datetime=generate_datetime(d["time"]), gateway_name=self.gateway_name, ) self.gateway.on_order(order) self.gateway.write_log("Order data received") def on_query_contract(self, data: dict, request: Request) -> None: """ Callback of available contracts query. This function processes the exchange info response and creates ContractData objects for each trading instrument. It handles different contract types and extracts trading rules like price tick, minimum/maximum volumes from filters. Parameters: data: Response data from the server request: Original request object """ for d in data["symbols"]: pricetick: float = 1 min_volume: float = 1 max_volume: float = 1 for f in d["filters"]: if f["filterType"] == "PRICE_FILTER": pricetick = float(f["tickSize"]) elif f["filterType"] == "LOT_SIZE": min_volume = float(f["minQty"]) max_volume = float(f["maxQty"]) product: Product | None = PRODUCT_BINANCE2VT.get(d["contractType"], None) if product == Product.SWAP: symbol: str = d["symbol"].replace("_PERP", "") + "_SWAP_BINANCE" elif product == Product.FUTURES: symbol = d["symbol"] + "_BINANCE" else: continue contract: ContractData = ContractData( symbol=symbol, exchange=Exchange.GLOBAL, name=d["symbol"], pricetick=pricetick, size=1, min_volume=min_volume, max_volume=max_volume, product=PRODUCT_BINANCE2VT.get(d["contractType"], Product.SWAP), net_position=True, history_data=True, gateway_name=self.gateway_name, stop_supported=True ) self.gateway.on_contract(contract) self.gateway.write_log("Contract data received") # Query private data after time offset is calculated if self.key and self.secret: self.query_order() self.query_account() self.query_position() self.start_user_stream() def on_start_user_stream(self, data: dict, request: Request) -> None: """ Successful callback of start_user_stream. This function processes the listen key response and initializes the user data websocket connection with the provided key. Parameters: data: Response data from the server containing the listen key request: Original request object """ self.user_stream_key = data["listenKey"] self.keep_alive_count = 0 if self.server == "REAL": url = REAL_USER_HOST + self.user_stream_key else: url = TESTNET_USER_HOST + self.user_stream_key self.user_api.connect(url, self.proxy_host, self.proxy_port) def on_keep_user_stream(self, data: dict, request: Request) -> None: """ Successful callback of keep_user_stream. This function handles the successful response of the listen key refresh request. No action is needed on success. Parameters: data: Response data from the server request: Original request object """ pass def on_keep_user_stream_error(self, exception_type: type, exception_value: Exception, tb: Any, request: Request) -> None: """ Error callback of keep_user_stream. This function handles errors from the listen key refresh request. Timeout exceptions are ignored as they are common and non-critical. Parameters: exception_type: Type of the exception exception_value: Exception instance tb: Traceback object request: Original request object """ if not issubclass(exception_type, TimeoutError): # Ignore timeout exception self.on_error(exception_type, exception_value, tb, request) def query_history(self, req: HistoryRequest) -> list[BarData]: """Query kline history data""" # Check if the contract and interval exist contract: ContractData | None = self.gateway.get_contract_by_symbol(req.symbol) if not contract: return [] if not req.interval: return [] # Prepare history list history: list[BarData] = [] limit: int = 1500 interval_delta: timedelta = TIMEDELTA_MAP[req.interval] page_span: timedelta = interval_delta * (limit - 1) current_start_dt: datetime = req.start while True: if req.end and current_start_dt >= req.end: break # Create query parameters params: dict = { "symbol": contract.name, "interval": INTERVAL_VT2BINANCE[req.interval], "limit": limit } params["startTime"] = int(datetime.timestamp(current_start_dt)) * 1000 path: str = "/dapi/v1/klines" if req.end: page_end_dt: datetime = min(req.end, current_start_dt + page_span) params["endTime"] = int(datetime.timestamp(page_end_dt)) * 1000 resp: Response = self.request( "GET", path=path, params=params ) # Break the loop if request failed if resp.status_code // 100 != 2: msg: str = f"Query kline history failed, status code: {resp.status_code}, message: {resp.text}" self.gateway.write_log(msg) break else: data: list = resp.json() if not data: msg = f"No kline history data is received, start time: {current_start_dt}" self.gateway.write_log(msg) break buf: list[BarData] = [] for row in data: bar: BarData = BarData( symbol=req.symbol, exchange=req.exchange, datetime=generate_datetime(row[0]), interval=req.interval, volume=float(row[5]), turnover=float(row[7]), open_price=float(row[1]), high_price=float(row[2]), low_price=float(row[3]), close_price=float(row[4]), gateway_name=self.gateway_name ) bar.extra = { "trade_count": int(row[8]), "active_volume": float(row[9]), "active_turnover": float(row[10]), } buf.append(bar) begin_dt: datetime = buf[0].datetime end_dt: datetime = buf[-1].datetime history.extend(buf) msg = f"Query kline history finished, {req.symbol} - {req.interval.value}, {begin_dt} - {end_dt}" self.gateway.write_log(msg) next_start_dt: datetime = end_dt + interval_delta if next_start_dt <= current_start_dt: msg = ( "Query kline history pagination stopped because received data " f"did not advance, start: {current_start_dt}, end: {end_dt}" ) self.gateway.write_log(msg) break # Break the loop if the latest data received if ( len(data) < limit or (req.end and next_start_dt >= req.end) ): break # Update query start time current_start_dt = next_start_dt # Wait to meet request flow limit sleep(0.5) # Remove the unclosed kline if history: history.pop(-1) return history class UserApi(WebsocketClient): """ The user data websocket API of BinanceInverseGateway. This class handles user data events from Binance through websocket connection. It processes real-time updates for: - Account balance changes - Position updates - Order status changes - Trade executions """ def __init__(self, gateway: BinanceInverseGateway) -> None: """ The init method of the API. This method initializes the websocket client with a reference to the parent gateway. Parameters: gateway: the parent gateway object for pushing callback data """ super().__init__() self.gateway: BinanceInverseGateway = gateway self.gateway_name: str = gateway.gateway_name def connect(self, url: str, proxy_host: str, proxy_port: int) -> None: """ Start server connection. This method establishes a websocket connection to Binance user data stream. Parameters: url: Websocket endpoint URL with listen key proxy_host: Proxy server hostname or IP proxy_port: Proxy server port """ self.init(url, proxy_host, proxy_port, receive_timeout=WEBSOCKET_TIMEOUT) self.start() def on_connected(self) -> None: """ Callback when server is connected. This function is called when the websocket connection to the server is successfully established. It logs the connection status. """ self.gateway.write_log("User API connected") def on_packet(self, packet: dict) -> None: """ Callback of data update. This function processes websocket messages from the user data stream. It handles different event types including account updates, order updates, and listen key expiration. Parameters: packet: JSON data received from websocket """ match packet["e"]: case "ACCOUNT_UPDATE": self.on_account(packet) case "ORDER_TRADE_UPDATE": self.on_order(packet) case "listenKeyExpired": self.on_listen_key_expired() def on_listen_key_expired(self) -> None: """ Callback of listen key expired. This function is called when the exchange notifies that the listen key has expired. It will log a message and disconnect the websocket connection. """ self.gateway.write_log("Listen key expired") self.disconnect() def on_account(self, packet: dict) -> None: """ Callback of account balance and holding position update. This function processes the account update event from the user data stream, including balance changes and position updates. Parameters: packet: JSON data received from websocket """ for acc_data in packet["a"]["B"]: account: AccountData = AccountData( accountid=acc_data["a"], balance=float(acc_data["wb"]), frozen=float(acc_data["wb"]) - float(acc_data["cw"]), gateway_name=self.gateway_name ) if account.balance: self.gateway.on_account(account) for pos_data in packet["a"]["P"]: if pos_data["ps"] == "BOTH": volume = pos_data["pa"] if "." in volume: volume = float(volume) else: volume = int(volume) name: str = pos_data["s"] contract: ContractData | None = self.gateway.get_contract_by_name(name) if not contract: continue position: PositionData = PositionData( symbol=contract.symbol, exchange=Exchange.GLOBAL, direction=Direction.NET, volume=volume, price=float(pos_data["ep"]), pnl=float(pos_data["up"]), gateway_name=self.gateway_name, ) self.gateway.on_position(position) def on_order(self, packet: dict) -> None: """ Callback of order and trade update. This function processes the order update event from the user data stream, including order status changes and trade executions. Parameters: packet: JSON data received from websocket """ ord_data: dict = packet["o"] # Filter unsupported order type key: tuple[str, str] = (ord_data["o"], ord_data["f"]) order_type: OrderType | None = ORDERTYPE_BINANCE2VT.get(key, None) if not order_type: return # Filter unsupported symbol name: str = ord_data["s"] contract: ContractData | None = self.gateway.get_contract_by_name(name) if not contract: return # Create and push order order: OrderData = OrderData( symbol=contract.symbol, exchange=Exchange.GLOBAL, orderid=str(ord_data["c"]), type=order_type, direction=DIRECTION_BINANCE2VT[ord_data["S"]], price=float(ord_data["p"]), volume=float(ord_data["q"]), traded=float(ord_data["z"]), status=STATUS_BINANCE2VT[ord_data["X"]], datetime=generate_datetime(packet["E"]), gateway_name=self.gateway_name, ) self.gateway.on_order(order) # Round trade volume to meet step size trade_volume: float = float(ord_data["l"]) trade_volume = round_to(trade_volume, contract.min_volume) if not trade_volume: return # Create and push trade trade: TradeData = TradeData( symbol=order.symbol, exchange=order.exchange, orderid=order.orderid, tradeid=ord_data["t"], direction=order.direction, price=float(ord_data["L"]), volume=trade_volume, datetime=generate_datetime(ord_data["T"]), gateway_name=self.gateway_name, ) self.gateway.on_trade(trade) def on_disconnected(self, status_code: int, msg: str) -> None: """ Callback when server is disconnected. This function is called when the websocket connection is closed. It logs the disconnection details and attempts to restart the user stream. Parameters: status_code: HTTP status code for the disconnection msg: Disconnection message """ self.gateway.write_log(f"User API disconnected, code: {status_code}, msg: {msg}") self.gateway.rest_api.start_user_stream() def on_error(self, e: Exception) -> None: """ Callback when exception raised. This function is called when an exception occurs in the websocket connection. It logs the exception details for troubleshooting. Parameters: e: The exception that was raised """ self.gateway.write_log(f"User API exception: {e}") class MdApi(WebsocketClient): """ The market data websocket API of BinanceInverseGateway. This class handles market data from Binance through websocket connection. It processes real-time updates for: - Tickers (24hr statistics) - Order book depth (10 levels) - Klines (candlestick data) if enabled """ def __init__(self, gateway: BinanceInverseGateway) -> None: """ The init method of the API. This method initializes the websocket client with a reference to the parent gateway. Parameters: gateway: the parent gateway object for pushing callback data """ super().__init__() self.gateway: BinanceInverseGateway = gateway self.gateway_name: str = gateway.gateway_name self.ticks: dict[str, TickData] = {} self.reqid: int = 0 self.kline_stream: bool = False self.new_channels: list[str] = [] def connect( self, server: str, kline_stream: bool, proxy_host: str, proxy_port: int, ) -> None: """ Start server connection. This method establishes a websocket connection to Binance market data stream. Parameters: server: Server type ("REAL" or "TESTNET") kline_stream: Whether to include kline data stream proxy_host: Proxy server hostname or IP proxy_port: Proxy server port """ self.kline_stream = kline_stream if server == "REAL": self.init(REAL_DATA_HOST, proxy_host, proxy_port, receive_timeout=WEBSOCKET_TIMEOUT) else: self.init(TESTNET_DATA_HOST, proxy_host, proxy_port, receive_timeout=WEBSOCKET_TIMEOUT) self.start() def on_connected(self) -> None: """ Callback when server is connected. This function is called when the market data websocket connection is successfully established. It logs the connection status and resubscribes to previously subscribed market data channels. """ self.gateway.write_log("MD API connected") # Resubscribe market data if self.ticks: channels = [] for symbol in self.ticks.keys(): channels.append(f"{symbol}@ticker") channels.append(f"{symbol}@depth10") if self.kline_stream: channels.append(f"{symbol}@kline_1m") packet: dict = { "method": "SUBSCRIBE", "params": channels, "id": self.reqid } self.send_packet(packet) def subscribe(self, req: SubscribeRequest) -> None: """ Subscribe to market data. This function sends subscription requests for ticker and depth data for the specified trading instrument. If kline_stream is enabled, it will also subscribe to 1-minute kline data. Parameters: req: Subscription request object containing symbol information """ if req.symbol in self.ticks: return contract: ContractData | None = self.gateway.get_contract_by_symbol(req.symbol) if not contract: self.gateway.write_log(f"Failed to subscribe data, symbol not found: {req.symbol}") return self.reqid += 1 # Initialize tick object tick: TickData = TickData( symbol=req.symbol, name=contract.name, exchange=Exchange.GLOBAL, datetime=datetime.now(UTC_TZ), gateway_name=self.gateway_name, ) tick.extra = {} self.ticks[req.symbol] = tick channels: list[str] = [ f"{contract.name.lower()}@ticker", f"{contract.name.lower()}@depth10" ] if self.kline_stream: channels.append(f"{contract.name.lower()}@kline_1m") self.new_channels.extend(channels) def subscribe_new_channels(self) -> None: """ Update timer event. This function sends subscription requests for new channels to the market data websocket server. """ if not self.new_channels: return packet: dict = { "method": "SUBSCRIBE", "params": self.new_channels, "id": self.reqid } self.send_packet(packet) self.new_channels = [] def on_packet(self, packet: dict) -> None: """ Callback of market data update. This function processes different types of market data updates, including ticker, depth, and kline data. It updates the corresponding TickData object and pushes updates to the gateway. Parameters: packet: JSON data received from websocket """ stream: str | None = packet.get("stream", None) if not stream: return data: dict = packet["data"] name, channel = stream.split("@") contract: ContractData | None = self.gateway.get_contract_by_name(name.upper()) if not contract: return tick: TickData = self.ticks[contract.symbol] if channel == "ticker": tick.volume = float(data["v"]) tick.turnover = float(data["q"]) tick.open_price = float(data["o"]) tick.high_price = float(data["h"]) tick.low_price = float(data["l"]) tick.last_price = float(data["c"]) tick.datetime = generate_datetime(float(data["E"])) elif channel == "depth10": bids: list = data["b"] for n in range(min(10, len(bids))): price, volume = bids[n] tick.__setattr__("bid_price_" + str(n + 1), float(price)) tick.__setattr__("bid_volume_" + str(n + 1), float(volume)) asks: list = data["a"] for n in range(min(10, len(asks))): price, volume = asks[n] tick.__setattr__("ask_price_" + str(n + 1), float(price)) tick.__setattr__("ask_volume_" + str(n + 1), float(volume)) tick.datetime = generate_datetime(float(data["E"])) else: kline_data: dict = data["k"] # Check if bar is closed bar_ready: bool = kline_data.get("x", False) if not bar_ready: return if tick.extra is None: tick.extra = {} dt: datetime = generate_datetime(float(kline_data["t"])) tick.extra["bar"] = BarData( symbol=name.upper(), exchange=Exchange.GLOBAL, datetime=dt.replace(second=0, microsecond=0), interval=Interval.MINUTE, volume=float(kline_data["v"]), turnover=float(kline_data["q"]), open_price=float(kline_data["o"]), high_price=float(kline_data["h"]), low_price=float(kline_data["l"]), close_price=float(kline_data["c"]), gateway_name=self.gateway_name ) if tick.last_price: tick.localtime = datetime.now() self.gateway.on_tick(copy(tick)) def on_disconnected(self, status_code: int, msg: str) -> None: """ Callback when server is disconnected. This function is called when the market data websocket connection is closed. It logs the disconnection details. Parameters: status_code: HTTP status code for the disconnection msg: Disconnection message """ self.gateway.write_log(f"MD API disconnected, code: {status_code}, msg: {msg}") def on_error(self, e: Exception) -> None: """ Callback when exception raised. This function is called when an exception occurs in the market data websocket connection. It logs the exception details for troubleshooting. Parameters: e: The exception that was raised """ self.gateway.write_log(f"MD API exception: {e}") class TradeApi(WebsocketClient): """ The trading websocket API of BinanceInverseGateway. This class handles trading operations with Binance through websocket connection. It provides functionality for: - Order placement - Order cancellation - Request authentication and signature generation """ def __init__(self, gateway: BinanceInverseGateway) -> None: """ The init method of the API. This method initializes the websocket client with a reference to the parent gateway. Parameters: gateway: the parent gateway object for pushing callback data """ super().__init__() self.gateway: BinanceInverseGateway = gateway self.gateway_name: str = gateway.gateway_name self.key: str = "" self.secret: bytes = b"" self.proxy_port: int = 0 self.proxy_host: str = "" self.server: str = "" self.reqid: int = 0 self.order_count: int = 0 self.order_prefix: str = "" self.reqid_callback_map: dict[int, Callable] = {} self.reqid_order_map: dict[int, OrderData] = {} def connect( self, key: str, secret: str, server: str, proxy_host: str, proxy_port: int ) -> None: """ Start server connection. This method initializes the API credentials and establishes a websocket connection to Binance trading API. Parameters: key: API Key for authentication secret: API Secret for request signing server: Server type ("REAL" or "TESTNET") proxy_host: Proxy server hostname or IP proxy_port: Proxy server port """ self.key = key self.secret = secret.encode() self.proxy_port = proxy_port self.proxy_host = proxy_host self.server = server self.order_prefix = datetime.now().strftime("%y%m%d%H%M%S") if self.server == "REAL": self.init(REAL_TRADE_HOST, proxy_host, proxy_port) else: self.init(TESTNET_TRADE_HOST, proxy_host, proxy_port) self.start() def sign(self, params: dict) -> None: """ Generate the signature for the request. This function creates an HMAC-SHA256 signature required for authenticated API requests to Binance. Parameters: params: Dictionary containing the parameters to be signed """ timestamp: int = int(time.time() * 1000) params["timestamp"] = timestamp payload: str = "&".join([f"{k}={v}" for k, v in sorted(params.items())]) signature: str = hmac.new( self.secret, payload.encode("utf-8"), hashlib.sha256 ).hexdigest() params["signature"] = signature def send_order(self, req: OrderRequest) -> str: """ Send new order to Binance. This function creates and sends a new order request to the exchange. It handles different order types including market, limit, and stop orders. Parameters: req: Order request object containing order details Returns: vt_orderid: The VeighNa order ID (gateway_name.orderid) if successful, empty string otherwise """ # Get contract contract: ContractData | None = self.gateway.get_contract_by_symbol(req.symbol) if not contract: self.gateway.write_log(f"Failed to send order, symbol not found: {req.symbol}") return "" # Generate new order id self.order_count += 1 orderid: str = self.order_prefix + str(self.order_count) # Push a submitting order event order: OrderData = req.create_order_data( orderid, self.gateway_name ) self.gateway.on_order(order) # Create order parameters params: dict = { "apiKey": self.key, "symbol": contract.name, "side": DIRECTION_VT2BINANCE[req.direction], "quantity": format_float(req.volume), "newClientOrderId": orderid, } if req.type == OrderType.MARKET: params["type"] = "MARKET" elif req.type == OrderType.STOP: params["type"] = "STOP_MARKET" params["stopPrice"] = format_float(req.price) else: order_type, time_condition = ORDERTYPE_VT2BINANCE[req.type] params["type"] = order_type params["timeInForce"] = time_condition params["price"] = format_float(req.price) self.sign(params) self.reqid += 1 self.reqid_callback_map[self.reqid] = self.on_send_order self.reqid_order_map[self.reqid] = order packet: dict = { "id": self.reqid, "method": "order.place", "params": params, } self.send_packet(packet) return order.vt_orderid def cancel_order(self, req: CancelRequest) -> None: """ Cancel existing order on Binance. This function sends a request to cancel an existing order on the exchange. Parameters: req: Cancel request object containing order details """ contract: ContractData | None = self.gateway.get_contract_by_symbol(req.symbol) if not contract: self.gateway.write_log(f"Failed to cancel order, symbol not found: {req.symbol}") return params: dict = { "apiKey": self.key, "symbol": contract.name, "origClientOrderId": req.orderid } self.sign(params) self.reqid += 1 self.reqid_callback_map[self.reqid] = self.on_cancel_order packet: dict = { "id": self.reqid, "method": "order.cancel", "params": params, } self.send_packet(packet) def on_connected(self) -> None: """ Callback when server is connected. This function is called when the trading websocket connection is successfully established. It logs the connection status. """ self.gateway.write_log("Trade API connected") def on_disconnected(self, status_code: int, msg: str) -> None: """ Callback when server is disconnected. This function is called when the trading websocket connection is closed. It logs the disconnection details. Parameters: status_code: HTTP status code for the disconnection msg: Disconnection message """ self.gateway.write_log(f"Trade API disconnected, code: {status_code}, msg: {msg}") def on_packet(self, packet: dict) -> None: """ Callback of data update. This function processes responses from the trading websocket API. It routes the response to the appropriate callback function based on the request ID. Parameters: packet: JSON data received from websocket """ reqid: int = packet.get("id", 0) callback: Callable | None = self.reqid_callback_map.get(reqid, None) if callback: callback(packet) def on_send_order(self, packet: dict) -> None: """ Callback of send order. This function processes the response to an order placement request. It handles errors by logging the details and updating the order status. Parameters: packet: JSON data received from websocket """ error: dict | None = packet.get("error", None) if not error: return error_code: str = error["code"] error_msg: str = error["msg"] msg: str = f"Order rejected, code: {error_code}, message: {error_msg}" self.gateway.write_log(msg) reqid: int = packet.get("id", 0) order: OrderData | None = self.reqid_order_map.get(reqid, None) if order: order.status = Status.REJECTED self.gateway.on_order(order) def on_cancel_order(self, packet: dict) -> None: """ Callback of cancel order. This function processes the response to an order cancellation request. It handles errors by logging the details. Parameters: packet: JSON data received from websocket """ error: dict | None = packet.get("error", None) if not error: return error_code: str = error["code"] error_msg: str = error["msg"] msg: str = f"Cancel rejected, code: {error_code}, message: {error_msg}" self.gateway.write_log(msg) def on_error(self, e: Exception) -> None: """ Callback when exception raised. This function is called when an exception occurs in the trading websocket connection. It logs the exception details for troubleshooting. Parameters: e: The exception that was raised """ self.gateway.write_log(f"Trade API exception: {e}") def generate_datetime(timestamp: float) -> datetime: """ Generate datetime object from Binance timestamp. This function converts a Binance millisecond timestamp to a datetime object with UTC timezone. Parameters: timestamp: Binance timestamp in milliseconds Returns: Datetime object with UTC timezone """ dt: datetime = datetime.fromtimestamp(timestamp / 1000, tz=UTC_TZ) return dt def format_float(f: float) -> str: """ Convert float number to string with correct precision. This function formats floating point numbers to avoid precision errors when sending requests to Binance. Parameters: f: The floating point number to format Returns: Formatted string representation of the number Note: Fixes potential error -1111: Parameter "quantity" has too much precision """ return format_float_positional(f, trim="-") ================================================ FILE: vnpy_binance/linear_gateway.py ================================================ import hashlib import hmac import time import urllib.parse from copy import copy from typing import Any from collections.abc import Callable from time import sleep from datetime import datetime, timedelta from numpy import format_float_positional from vnpy.event import Event, EventEngine from vnpy.trader.constant import ( Direction, Exchange, Product, Status, OrderType, Interval ) from vnpy.trader.gateway import BaseGateway from vnpy.trader.object import ( TickData, OrderData, TradeData, AccountData, ContractData, PositionData, BarData, OrderRequest, CancelRequest, SubscribeRequest, HistoryRequest ) from vnpy.trader.event import EVENT_TIMER from vnpy.trader.utility import round_to, ZoneInfo from vnpy_rest import Request, RestClient, Response from vnpy_websocket import WebsocketClient # Timezone constant UTC_TZ = ZoneInfo("UTC") # Real server hosts REAL_REST_HOST: str = "https://fapi.binance.com" REAL_TRADE_HOST: str = "wss://ws-fapi.binance.com/ws-fapi/v1" REAL_USER_HOST: str = "wss://fstream.binance.com/private/ws" REAL_PUBLIC_HOST: str = "wss://fstream.binance.com/public/stream" REAL_MARKET_HOST: str = "wss://fstream.binance.com/market/stream" # Testnet server hosts TESTNET_REST_HOST: str = "https://demo-fapi.binance.com" TESTNET_TRADE_HOST: str = "wss://testnet.binancefuture.com/ws-fapi/v1" TESTNET_USER_HOST: str = "wss://fstream.binancefuture.com/private/ws" TESTNET_PUBLIC_HOST: str = "wss://fstream.binancefuture.com/public/stream" TESTNET_MARKET_HOST: str = "wss://fstream.binancefuture.com/market/stream" # Order status map STATUS_BINANCE2VT: dict[str, Status] = { "NEW": Status.NOTTRADED, "PARTIALLY_FILLED": Status.PARTTRADED, "FILLED": Status.ALLTRADED, "CANCELED": Status.CANCELLED, "REJECTED": Status.REJECTED, "EXPIRED": Status.CANCELLED } # Order type map ORDERTYPE_VT2BINANCE: dict[OrderType, tuple[str, str]] = { OrderType.LIMIT: ("LIMIT", "GTC"), OrderType.MARKET: ("MARKET", "GTC"), OrderType.FAK: ("LIMIT", "IOC"), OrderType.FOK: ("LIMIT", "FOK"), } ORDERTYPE_BINANCE2VT: dict[tuple[str, str], OrderType] = {v: k for k, v in ORDERTYPE_VT2BINANCE.items()} # Direction map DIRECTION_VT2BINANCE: dict[Direction, str] = { Direction.LONG: "BUY", Direction.SHORT: "SELL" } DIRECTION_BINANCE2VT: dict[str, Direction] = {v: k for k, v in DIRECTION_VT2BINANCE.items()} # Product map PRODUCT_BINANCE2VT: dict[str, Product] = { "PERPETUAL": Product.SWAP, "PERPETUAL_DELIVERING": Product.SWAP, "TRADIFI_PERPETUAL": Product.SWAP, "CURRENT_MONTH": Product.FUTURES, "NEXT_MONTH": Product.FUTURES, "CURRENT_QUARTER": Product.FUTURES, "NEXT_QUARTER": Product.FUTURES, } # Kline interval map INTERVAL_VT2BINANCE: dict[Interval, str] = { Interval.MINUTE: "1m", Interval.HOUR: "1h", Interval.DAILY: "1d", } # Timedelta map TIMEDELTA_MAP: dict[Interval, timedelta] = { Interval.MINUTE: timedelta(minutes=1), Interval.HOUR: timedelta(hours=1), Interval.DAILY: timedelta(days=1), } # Set weboscket timeout to 24 hour WEBSOCKET_TIMEOUT = 24 * 60 * 60 class BinanceLinearGateway(BaseGateway): """ The Binance linear trading gateway for VeighNa. This gateway provides trading functionality for Binance USDT perpetual contracts and delivery futures through their API. Features: 1. Only support crossed position 2. Only support one-way mode 3. Provides market data, trading, and account management capabilities """ default_name: str = "BINANCE_LINEAR" default_setting: dict = { "API Key": "", "API Secret": "", "Server": ["REAL", "TESTNET"], "Kline Stream": ["False", "True"], "Proxy Host": "", "Proxy Port": 0 } exchanges: list[Exchange] = [Exchange.GLOBAL] def __init__(self, event_engine: EventEngine, gateway_name: str) -> None: """ The init method of the gateway. This method initializes the gateway components including REST API, trading API, user data API, and market data API. It also sets up the data structures for order and contract storage. Parameters: event_engine: the global event engine object of VeighNa gateway_name: the unique name for identifying the gateway """ super().__init__(event_engine, gateway_name) self.trade_api: TradeApi = TradeApi(self) self.user_api: UserApi = UserApi(self) self.md_api: MdApi = MdApi(self) self.rest_api: RestApi = RestApi(self) self.orders: dict[str, OrderData] = {} self.symbol_contract_map: dict[str, ContractData] = {} self.name_contract_map: dict[str, ContractData] = {} def connect(self, setting: dict) -> None: """ Start server connections. This method establishes connections to Binance servers using the provided settings. Parameters: setting: A dictionary containing connection parameters including API credentials, server selection, and proxy configuration """ key: str = setting["API Key"] secret: str = setting["API Secret"] server: str = setting["Server"] kline_stream: bool = setting["Kline Stream"] == "True" proxy_host: str = setting["Proxy Host"] proxy_port: int = setting["Proxy Port"] self.rest_api.connect(key, secret, server, proxy_host, proxy_port) self.trade_api.connect(key, secret, server, proxy_host, proxy_port) self.md_api.connect(server, kline_stream, proxy_host, proxy_port) self.event_engine.register(EVENT_TIMER, self.process_timer_event) def subscribe(self, req: SubscribeRequest) -> None: """ Subscribe to market data. This method forwards the subscription request to the market data API. Parameters: req: Subscription request object containing the symbol to subscribe """ self.md_api.subscribe(req) def send_order(self, req: OrderRequest) -> str: """ Send new order. This method forwards the order request to the trading API. Parameters: req: Order request object containing order details Returns: str: The VeighNa order ID if successful, empty string if failed """ return self.trade_api.send_order(req) def cancel_order(self, req: CancelRequest) -> None: """ Cancel existing order. This method forwards the cancellation request to the trading API. Parameters: req: Cancel request object containing order details """ self.trade_api.cancel_order(req) def query_account(self) -> None: """ Query account balance. Not required since Binance provides websocket updates for account balances. """ pass def query_position(self) -> None: """ Query current positions. Not required since Binance provides websocket updates for positions. """ pass def query_history(self, req: HistoryRequest) -> list[BarData]: """ Query historical kline data. This method forwards the history request to the REST API. Parameters: req: History request object containing query parameters Returns: list[BarData]: List of historical kline data bars """ return self.rest_api.query_history(req) def close(self) -> None: """ Close server connections. This method stops all API connections and releases resources. """ self.rest_api.stop() self.user_api.stop() self.md_api.stop() self.trade_api.stop() def process_timer_event(self, event: Event) -> None: """ Process timer task. This function is called regularly by the event engine to perform scheduled tasks, such as keeping the user stream alive. Parameters: event: Timer event object """ self.rest_api.keep_user_stream() self.md_api.subscribe_new_channels() def on_order(self, order: OrderData) -> None: """ Save a copy of order and then push to event engine. Parameters: order: Order data object """ self.orders[order.orderid] = copy(order) super().on_order(order) def get_order(self, orderid: str) -> OrderData | None: """ Get previously saved order by order id. Parameters: orderid: The ID of the order to retrieve Returns: Order data object if found, None otherwise """ return self.orders.get(orderid, None) def on_contract(self, contract: ContractData) -> None: """ Save contract data in mappings and push to event engine. Parameters: contract: Contract data object """ self.symbol_contract_map[contract.symbol] = contract self.name_contract_map[contract.name] = contract super().on_contract(contract) def get_contract_by_symbol(self, symbol: str) -> ContractData | None: """ Get contract data by VeighNa symbol. Parameters: symbol: VeighNa symbol (e.g. "BTC_SWAP_BINANCE") Returns: Contract data object if found, None otherwise """ return self.symbol_contract_map.get(symbol, None) def get_contract_by_name(self, name: str) -> ContractData | None: """ Get contract data by exchange symbol name. Parameters: name: Exchange symbol name (e.g. "BTCUSDT") Returns: Contract data object if found, None otherwise """ return self.name_contract_map.get(name, None) class RestApi(RestClient): """ The REST API of BinanceLinearGateway. This class handles HTTP requests to Binance API endpoints, including: - Authentication and signature generation - Contract information queries - Account and position queries - Order management - Historical data queries - User data stream management """ def __init__(self, gateway: BinanceLinearGateway) -> None: """ The init method of the API. This method initializes the REST API with a reference to the parent gateway. Parameters: gateway: the parent gateway object for pushing callback data """ super().__init__() self.gateway: BinanceLinearGateway = gateway self.gateway_name: str = gateway.gateway_name self.user_api: UserApi = self.gateway.user_api self.key: str = "" self.secret: bytes = b"" self.user_stream_key: str = "" self.keep_alive_count: int = 0 self.time_offset: int = 0 self.order_count: int = 1_000_000 self.order_prefix: str = "" def sign(self, request: Request) -> Request: """ Standard callback for signing a request. This method adds the necessary authentication parameters and signature to requests that require API key authentication. It handles: 1. Path construction with query parameters 2. Timestamp generation with server time offset adjustment 3. HMAC-SHA256 signature generation 4. Required authentication headers Parameters: request: Request object to be signed Returns: Request: Modified request with authentication parameters """ # Construct path with query parameters if they exist if request.params: path: str = request.path + "?" + urllib.parse.urlencode(request.params) else: request.params = {} path = request.path # Get current timestamp in milliseconds timestamp: int = int(time.time() * 1000) # Adjust timestamp based on time offset with server if self.time_offset > 0: timestamp -= abs(self.time_offset) elif self.time_offset < 0: timestamp += abs(self.time_offset) # Add timestamp to request parameters request.params["timestamp"] = timestamp # Generate signature using HMAC SHA256 query: str = urllib.parse.urlencode(sorted(request.params.items())) signature: str = hmac.new( self.secret, query.encode("utf-8"), hashlib.sha256 ).hexdigest() # Append signature to query string query += f"&signature={signature}" path = request.path + "?" + query # Update request with signed path and clear params/data request.path = path request.params = {} request.data = {} # Add required headers for API authentication request.headers = { "Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json", "X-MBX-APIKEY": self.key, "Connection": "close" } return request def connect( self, key: str, secret: str, server: str, proxy_host: str, proxy_port: int ) -> None: """Start server connection""" self.key = key self.secret = secret.encode() self.proxy_port = proxy_port self.proxy_host = proxy_host self.server = server self.order_prefix = datetime.now().strftime("%y%m%d%H%M%S") if self.server == "REAL": self.init(REAL_REST_HOST, proxy_host, proxy_port) else: self.init(TESTNET_REST_HOST, proxy_host, proxy_port) self.start() self.gateway.write_log("REST API started") self.query_time() def query_time(self) -> None: """ Query server time to calculate local time offset. This function sends a request to get the exchange server time, which is used to calculate the local time offset for timestamp synchronization. """ path: str = "/fapi/v1/time" self.add_request( "GET", path, callback=self.on_query_time ) def query_account(self) -> None: """ Query account balance. This function sends a request to get the account balance information, including wallet balance, available balance, and margin. """ path: str = "/fapi/v3/account" self.add_request( method="GET", path=path, callback=self.on_query_account, ) def query_position(self) -> None: """ Query holding positions. This function sends a request to get current position data, including position amount, entry price, and unrealized profit/loss. """ path: str = "/fapi/v3/positionRisk" self.add_request( method="GET", path=path, callback=self.on_query_position, ) def query_order(self) -> None: """ Query open orders. This function sends a request to get all active orders that have not been fully filled or cancelled. """ path: str = "/fapi/v1/openOrders" self.add_request( method="GET", path=path, callback=self.on_query_order, ) def query_contract(self) -> None: """ Query available contracts. This function sends a request to get exchange information, including all available trading instruments, their precision, and trading rules. """ path: str = "/fapi/v1/exchangeInfo" self.add_request( method="GET", path=path, callback=self.on_query_contract, ) def start_user_stream(self) -> None: """ Create listen key for user stream. This function sends a request to create a listen key which is required to establish a user data websocket connection. """ path: str = "/fapi/v1/listenKey" self.add_request( method="POST", path=path, callback=self.on_start_user_stream, ) def keep_user_stream(self) -> None: """ Extend listen key validity. This function sends a request to keep the listen key active, which is required to maintain the user data websocket connection. The listen key will expire after 60 minutes if not refreshed. """ if not self.user_stream_key: return self.keep_alive_count += 1 if self.keep_alive_count < 600: return self.keep_alive_count = 0 params: dict = {"listenKey": self.user_stream_key} path: str = "/fapi/v1/listenKey" self.add_request( method="PUT", path=path, callback=self.on_keep_user_stream, params=params, on_error=self.on_keep_user_stream_error ) def on_query_time(self, data: dict, request: Request) -> None: """ Callback of server time query. This function processes the server time response and calculates the time offset between local and server time, which is used for request timestamp synchronization. Parameters: data: Response data from the server request: Original request object """ local_time: int = int(time.time() * 1000) server_time: int = int(data["serverTime"]) self.time_offset = local_time - server_time self.gateway.write_log(f"Server time updated, local offset: {self.time_offset}ms") self.query_contract() def on_query_account(self, data: dict, request: Request) -> None: """ Callback of account balance query. This function processes the account balance response and creates AccountData objects for each asset in the account. Parameters: data: Response data from the server request: Original request object """ for asset in data["assets"]: account: AccountData = AccountData( accountid=asset["asset"], balance=float(asset["walletBalance"]), frozen=float(asset["maintMargin"]), gateway_name=self.gateway_name ) self.gateway.on_account(account) self.gateway.write_log("Account data received") def on_query_position(self, data: list, request: Request) -> None: """ Callback of holding positions query. This function processes the position data response and creates PositionData objects for each position held. Parameters: data: Response data from the server request: Original request object """ for d in data: name: str = d["symbol"] contract: ContractData | None = self.gateway.get_contract_by_name(name) if not contract: continue position: PositionData = PositionData( symbol=contract.symbol, exchange=Exchange.GLOBAL, direction=Direction.NET, volume=float(d["positionAmt"]), price=float(d["entryPrice"]), pnl=float(d["unRealizedProfit"]), gateway_name=self.gateway_name, ) self.gateway.on_position(position) self.gateway.write_log("Position data received") def on_query_order(self, data: list, request: Request) -> None: """ Callback of open orders query. This function processes the open orders response and creates OrderData objects for each active order. Parameters: data: Response data from the server request: Original request object """ for d in data: key: tuple[str, str] = (d["type"], d["timeInForce"]) order_type: OrderType | None = ORDERTYPE_BINANCE2VT.get(key, None) if not order_type: continue contract: ContractData | None = self.gateway.get_contract_by_symbol(d["symbol"]) if not contract: continue order: OrderData = OrderData( orderid=d["clientOrderId"], symbol=contract.symbol, exchange=Exchange.GLOBAL, price=float(d["price"]), volume=float(d["origQty"]), type=order_type, direction=DIRECTION_BINANCE2VT[d["side"]], traded=float(d["executedQty"]), status=STATUS_BINANCE2VT[d["status"]], datetime=generate_datetime(d["time"]), gateway_name=self.gateway_name, ) self.gateway.on_order(order) self.gateway.write_log("Order data received") def on_query_contract(self, data: dict, request: Request) -> None: """ Callback of available contracts query. This function processes the exchange info response and creates ContractData objects for each trading instrument. It handles different contract types and extracts trading rules like price tick, minimum/maximum volumes from filters. Parameters: data: Response data from the server request: Original request object """ for d in data["symbols"]: pricetick: float = 1 min_volume: float = 1 max_volume: float = 1 for f in d["filters"]: if f["filterType"] == "PRICE_FILTER": pricetick = float(f["tickSize"]) elif f["filterType"] == "LOT_SIZE": min_volume = float(f["minQty"]) max_volume = float(f["maxQty"]) product: Product | None = PRODUCT_BINANCE2VT.get(d["contractType"], None) if product == Product.SWAP: symbol: str = d["symbol"] + "_SWAP_BINANCE" elif product == Product.FUTURES: symbol = d["symbol"] + "_BINANCE" else: continue contract: ContractData = ContractData( symbol=symbol, exchange=Exchange.GLOBAL, name=d["symbol"], pricetick=pricetick, size=1, min_volume=min_volume, max_volume=max_volume, product=product, net_position=True, history_data=True, gateway_name=self.gateway_name, stop_supported=False ) self.gateway.on_contract(contract) self.gateway.write_log("Contract data received") # Query private data after time offset is calculated if self.key and self.secret: self.query_order() self.query_account() self.query_position() self.start_user_stream() def on_start_user_stream(self, data: dict, request: Request) -> None: """ Successful callback of start_user_stream. This function processes the listen key response and initializes the user data websocket connection with the provided key. Parameters: data: Response data from the server containing the listen key request: Original request object """ self.user_stream_key = data["listenKey"] self.keep_alive_count = 0 params: str = urllib.parse.urlencode( { "listenKey": self.user_stream_key, "events": "ORDER_TRADE_UPDATE/ACCOUNT_UPDATE", }, safe="/" ) if self.server == "REAL": url = f"{REAL_USER_HOST}?{params}" else: url = f"{TESTNET_USER_HOST}?{params}" self.user_api.connect(url, self.proxy_host, self.proxy_port) def on_keep_user_stream(self, data: dict, request: Request) -> None: """ Successful callback of keep_user_stream. This function handles the successful response of the listen key refresh request. No action is needed on success. Parameters: data: Response data from the server request: Original request object """ pass def on_keep_user_stream_error(self, exception_type: type, exception_value: Exception, tb: Any, request: Request) -> None: """ Error callback of keep_user_stream. This function handles errors from the listen key refresh request. Timeout exceptions are ignored as they are common and non-critical. Parameters: exception_type: Type of the exception exception_value: Exception instance tb: Traceback object request: Original request object """ if not issubclass(exception_type, TimeoutError): # Ignore timeout exception self.on_error(exception_type, exception_value, tb, request) def query_history(self, req: HistoryRequest) -> list[BarData]: """Query kline history data""" # Check if the contract and interval exist contract: ContractData | None = self.gateway.get_contract_by_symbol(req.symbol) if not contract: return [] if not req.interval: return [] # Prepare history list history: list[BarData] = [] limit: int = 1500 # Convert start time to milliseconds start_time: int = int(datetime.timestamp(req.start)) while True: # Create query parameters params: dict = { "symbol": contract.name, "interval": INTERVAL_VT2BINANCE[req.interval], "limit": limit } params["startTime"] = start_time * 1000 path: str = "/fapi/v1/klines" if req.end: end_time = int(datetime.timestamp(req.end)) params["endTime"] = end_time * 1000 # Convert to milliseconds resp: Response = self.request( "GET", path=path, params=params ) # Break the loop if request failed if resp.status_code // 100 != 2: msg: str = f"Query kline history failed, status code: {resp.status_code}, message: {resp.text}" self.gateway.write_log(msg) break else: data: dict = resp.json() if not data: msg = f"No kline history data is received, start time: {start_time}" self.gateway.write_log(msg) break buf: list[BarData] = [] for row in data: bar: BarData = BarData( symbol=req.symbol, exchange=req.exchange, datetime=generate_datetime(row[0]), interval=req.interval, volume=float(row[5]), turnover=float(row[7]), open_price=float(row[1]), high_price=float(row[2]), low_price=float(row[3]), close_price=float(row[4]), gateway_name=self.gateway_name ) bar.extra = { "trade_count": int(row[8]), "active_volume": float(row[9]), "active_turnover": float(row[10]), } buf.append(bar) begin: datetime = buf[0].datetime end: datetime = buf[-1].datetime history.extend(buf) msg = f"Query kline history finished, {req.symbol} - {req.interval.value}, {begin} - {end}" self.gateway.write_log(msg) next_start_dt = bar.datetime + TIMEDELTA_MAP[req.interval] next_start_time = int(datetime.timestamp(next_start_dt)) # Break the loop if the latest data received if ( len(data) < limit or (req.end and next_start_dt >= req.end) ): break # Update query start time start_time = next_start_time # Wait to meet request flow limit sleep(0.5) # Remove the unclosed kline if history: history.pop(-1) return history class UserApi(WebsocketClient): """ The user data websocket API of BinanceLinearGateway. This class handles user data events from Binance through websocket connection. It processes real-time updates for: - Account balance changes - Position updates - Order status changes - Trade executions """ def __init__(self, gateway: BinanceLinearGateway) -> None: """ The init method of the API. This method initializes the websocket client with a reference to the parent gateway. Parameters: gateway: the parent gateway object for pushing callback data """ super().__init__() self.gateway: BinanceLinearGateway = gateway self.gateway_name: str = gateway.gateway_name def connect(self, url: str, proxy_host: str, proxy_port: int) -> None: """ Start server connection. This method establishes a websocket connection to Binance user data stream. Parameters: url: Websocket endpoint URL with listen key proxy_host: Proxy server hostname or IP proxy_port: Proxy server port """ self.init(url, proxy_host, proxy_port, receive_timeout=WEBSOCKET_TIMEOUT) self.start() def on_connected(self) -> None: """ Callback when server is connected. This function is called when the websocket connection to the server is successfully established. It logs the connection status. """ self.gateway.write_log("User API connected") def on_packet(self, packet: dict) -> None: """ Callback of data update. This function processes websocket messages from the user data stream. It handles different event types including account updates, order updates, and listen key expiration. Parameters: packet: JSON data received from websocket """ match packet["e"]: case "ACCOUNT_UPDATE": self.on_account(packet) case "ORDER_TRADE_UPDATE": self.on_order(packet) case "listenKeyExpired": self.on_listen_key_expired() def on_listen_key_expired(self) -> None: """ Callback of listen key expired. This function is called when the exchange notifies that the listen key has expired. It will log a message and disconnect the websocket connection. """ self.gateway.write_log("Listen key expired") def on_account(self, packet: dict) -> None: """ Callback of account balance and holding position update. This function processes the account update event from the user data stream, including balance changes and position updates. Parameters: packet: JSON data received from websocket """ for acc_data in packet["a"]["B"]: account: AccountData = AccountData( accountid=acc_data["a"], balance=float(acc_data["wb"]), frozen=float(acc_data["wb"]) - float(acc_data["cw"]), gateway_name=self.gateway_name ) if account.balance: self.gateway.on_account(account) for pos_data in packet["a"]["P"]: if pos_data["ps"] == "BOTH": volume = pos_data["pa"] if "." in volume: volume = float(volume) else: volume = int(volume) name: str = pos_data["s"] contract: ContractData | None = self.gateway.get_contract_by_name(name) if not contract: continue position: PositionData = PositionData( symbol=contract.symbol, exchange=Exchange.GLOBAL, direction=Direction.NET, volume=volume, price=float(pos_data["ep"]), pnl=float(pos_data["up"]), gateway_name=self.gateway_name, ) self.gateway.on_position(position) def on_order(self, packet: dict) -> None: """ Callback of order and trade update. This function processes the order update event from the user data stream, including order status changes and trade executions. Parameters: packet: JSON data received from websocket """ ord_data: dict = packet["o"] # Filter unsupported order type key: tuple[str, str] = (ord_data["o"], ord_data["f"]) order_type: OrderType | None = ORDERTYPE_BINANCE2VT.get(key, None) if not order_type: return # Filter unsupported symbol name: str = ord_data["s"] contract: ContractData | None = self.gateway.get_contract_by_name(name) if not contract: return # Create and push order order: OrderData = OrderData( symbol=contract.symbol, exchange=Exchange.GLOBAL, orderid=str(ord_data["c"]), type=order_type, direction=DIRECTION_BINANCE2VT[ord_data["S"]], price=float(ord_data["p"]), volume=float(ord_data["q"]), traded=float(ord_data["z"]), status=STATUS_BINANCE2VT[ord_data["X"]], datetime=generate_datetime(packet["E"]), gateway_name=self.gateway_name, ) self.gateway.on_order(order) # Round trade volume to meet step size trade_volume: float = float(ord_data["l"]) trade_volume = round_to(trade_volume, contract.min_volume) if not trade_volume: return # Create and push trade trade: TradeData = TradeData( symbol=order.symbol, exchange=order.exchange, orderid=order.orderid, tradeid=ord_data["t"], direction=order.direction, price=float(ord_data["L"]), volume=trade_volume, datetime=generate_datetime(ord_data["T"]), gateway_name=self.gateway_name, ) self.gateway.on_trade(trade) def on_disconnected(self, status_code: int, msg: str) -> None: """ Callback when server is disconnected. This function is called when the websocket connection is closed. It logs the disconnection details and attempts to restart the user stream. Parameters: status_code: HTTP status code for the disconnection msg: Disconnection message """ self.gateway.write_log(f"User API disconnected, code: {status_code}, msg: {msg}") self.gateway.rest_api.start_user_stream() def on_error(self, e: Exception) -> None: """ Callback when exception raised. This function is called when an exception occurs in the websocket connection. It logs the exception details for troubleshooting. Parameters: e: The exception that was raised """ self.gateway.write_log(f"User API exception: {e}") class MdApi(WebsocketClient): """ The market data websocket API of BinanceLinearGateway. This class handles market data from Binance through websocket connection. It processes real-time updates for: - Tickers (24hr statistics) - Order book depth (10 levels) - Klines (candlestick data) if enabled """ def __init__(self, gateway: BinanceLinearGateway) -> None: """ The init method of the API. This method initializes the websocket client with a reference to the parent gateway. Parameters: gateway: the parent gateway object for pushing callback data """ super().__init__() self.gateway: BinanceLinearGateway = gateway self.gateway_name: str = gateway.gateway_name self.ticks: dict[str, TickData] = {} self.public_api: PublicApi = PublicApi(self) self.reqid: int = 0 self.kline_stream: bool = False self.new_public_channels: list[str] = [] self.new_market_channels: list[str] = [] def connect( self, server: str, kline_stream: bool, proxy_host: str, proxy_port: int, ) -> None: """ Start server connection. This method establishes a websocket connection to Binance market data stream. Parameters: server: Server type ("REAL" or "TESTNET") kline_stream: Whether to include kline data stream proxy_host: Proxy server hostname or IP proxy_port: Proxy server port """ self.kline_stream = kline_stream if server == "REAL": self.init(REAL_MARKET_HOST, proxy_host, proxy_port, receive_timeout=WEBSOCKET_TIMEOUT) self.public_api.connect(REAL_PUBLIC_HOST, proxy_host, proxy_port) else: self.init(TESTNET_MARKET_HOST, proxy_host, proxy_port, receive_timeout=WEBSOCKET_TIMEOUT) self.public_api.connect(TESTNET_PUBLIC_HOST, proxy_host, proxy_port) self.start() def stop(self) -> None: """Stop market data websocket connections.""" self.public_api.stop() super().stop() def get_public_channels(self, contract: ContractData) -> list[str]: """Generate public market data channels for a contract.""" return [f"{contract.name.lower()}@depth10"] def get_market_channels(self, contract: ContractData) -> list[str]: """Generate market data channels for a contract.""" channels: list[str] = [f"{contract.name.lower()}@ticker"] if self.kline_stream: channels.append(f"{contract.name.lower()}@kline_1m") if contract.product == Product.SWAP: channels.append(f"{contract.name.lower()}@markPrice") return channels def send_subscribe_packet(self, api: WebsocketClient, channels: list[str]) -> None: """Send a subscribe packet through the given websocket client.""" if not channels: return self.reqid += 1 packet: dict = { "method": "SUBSCRIBE", "params": channels, "id": self.reqid } api.send_packet(packet) def resubscribe_market_channels(self) -> None: """Resubscribe market channels after reconnect.""" channels: list[str] = [] for symbol in self.ticks.keys(): contract: ContractData | None = self.gateway.get_contract_by_symbol(symbol) if not contract: continue channels.extend(self.get_market_channels(contract)) self.send_subscribe_packet(self, channels) def resubscribe_public_channels(self) -> None: """Resubscribe public channels after reconnect.""" channels: list[str] = [] for symbol in self.ticks.keys(): contract: ContractData | None = self.gateway.get_contract_by_symbol(symbol) if not contract: continue channels.extend(self.get_public_channels(contract)) self.send_subscribe_packet(self.public_api, channels) def on_connected(self) -> None: """ Callback when server is connected. This function is called when the market data websocket connection is successfully established. It logs the connection status and resubscribes to previously subscribed market data channels. """ self.gateway.write_log("MD API connected") # Resubscribe market data if self.ticks: self.resubscribe_market_channels() def subscribe(self, req: SubscribeRequest) -> None: """ Subscribe to market data. This function sends subscription requests for ticker and depth data for the specified trading instrument. If kline_stream is enabled, it will also subscribe to 1-minute kline data. Parameters: req: Subscription request object containing symbol information """ if req.symbol in self.ticks: return contract: ContractData | None = self.gateway.get_contract_by_symbol(req.symbol) if not contract: self.gateway.write_log(f"Failed to subscribe data, symbol not found: {req.symbol}") return self.reqid += 1 # Initialize tick object tick: TickData = TickData( symbol=req.symbol, name=contract.name, exchange=Exchange.GLOBAL, datetime=datetime.now(UTC_TZ), gateway_name=self.gateway_name, ) tick.extra = {} self.ticks[req.symbol] = tick self.new_public_channels.extend(self.get_public_channels(contract)) self.new_market_channels.extend(self.get_market_channels(contract)) def subscribe_new_channels(self) -> None: """ Update timer event. This function sends subscription requests for new channels to the market data websocket server. """ self.send_subscribe_packet(self, self.new_market_channels) self.send_subscribe_packet(self.public_api, self.new_public_channels) self.new_market_channels = [] self.new_public_channels = [] def on_packet(self, packet: dict) -> None: """ Callback of market data update. This function processes different types of market data updates, including ticker, depth, and kline data. It updates the corresponding TickData object and pushes updates to the gateway. Parameters: packet: JSON data received from websocket """ stream: str | None = packet.get("stream", None) if not stream: return data: dict = packet["data"] name, channel = stream.split("@", 1) contract: ContractData | None = self.gateway.get_contract_by_name(name.upper()) if not contract: return tick: TickData = self.ticks[contract.symbol] if channel == "ticker": tick.volume = float(data["v"]) tick.turnover = float(data["q"]) tick.open_price = float(data["o"]) tick.high_price = float(data["h"]) tick.low_price = float(data["l"]) tick.last_price = float(data["c"]) tick.datetime = generate_datetime(float(data["E"])) elif channel == "depth10": bids: list = data["b"] for n in range(min(10, len(bids))): price, volume = bids[n] tick.__setattr__("bid_price_" + str(n + 1), float(price)) tick.__setattr__("bid_volume_" + str(n + 1), float(volume)) asks: list = data["a"] for n in range(min(10, len(asks))): price, volume = asks[n] tick.__setattr__("ask_price_" + str(n + 1), float(price)) tick.__setattr__("ask_volume_" + str(n + 1), float(volume)) tick.datetime = generate_datetime(float(data["E"])) elif channel == "markPrice": if tick.extra is None: tick.extra = {} tick.extra["funding_rate"] = float(data["r"]) tick.extra["funding_time"] = int(data["T"]) else: kline_data: dict = data["k"] # Check if bar is closed bar_ready: bool = kline_data.get("x", False) if not bar_ready: return if tick.extra is None: tick.extra = {} dt: datetime = generate_datetime(float(kline_data["t"])) tick.extra["bar"] = BarData( symbol=name.upper(), exchange=Exchange.GLOBAL, datetime=dt.replace(second=0, microsecond=0), interval=Interval.MINUTE, volume=float(kline_data["v"]), turnover=float(kline_data["q"]), open_price=float(kline_data["o"]), high_price=float(kline_data["h"]), low_price=float(kline_data["l"]), close_price=float(kline_data["c"]), gateway_name=self.gateway_name ) if tick.last_price: tick.localtime = datetime.now() self.gateway.on_tick(copy(tick)) def on_disconnected(self, status_code: int, msg: str) -> None: """ Callback when server is disconnected. This function is called when the market data websocket connection is closed. It logs the disconnection details. Parameters: status_code: HTTP status code for the disconnection msg: Disconnection message """ self.gateway.write_log(f"MD API disconnected, code: {status_code}, msg: {msg}") def on_error(self, e: Exception) -> None: """ Callback when exception raised. This function is called when an exception occurs in the market data websocket connection. It logs the exception details for troubleshooting. Parameters: e: The exception that was raised """ self.gateway.write_log(f"MD API exception: {e}") class PublicApi(WebsocketClient): """Public market data websocket connection for high-frequency streams.""" def __init__(self, md_api: MdApi) -> None: super().__init__() self.md_api: MdApi = md_api self.gateway: BinanceLinearGateway = md_api.gateway def connect(self, url: str, proxy_host: str, proxy_port: int) -> None: """Start public market data websocket connection.""" self.init(url, proxy_host, proxy_port, receive_timeout=WEBSOCKET_TIMEOUT) self.start() def on_connected(self) -> None: """Callback when public websocket is connected.""" self.gateway.write_log("Public API connected") if self.md_api.ticks: self.md_api.resubscribe_public_channels() def on_packet(self, packet: dict) -> None: """Forward packets to the shared market data parser.""" self.md_api.on_packet(packet) def on_disconnected(self, status_code: int, msg: str) -> None: """Callback when public websocket is disconnected.""" self.gateway.write_log(f"MD Public API disconnected, code: {status_code}, msg: {msg}") def on_error(self, e: Exception) -> None: """Callback when public websocket raises an exception.""" self.gateway.write_log(f"MD Public API exception: {e}") class TradeApi(WebsocketClient): """ The trading websocket API of BinanceLinearGateway. This class handles trading operations with Binance through websocket connection. It provides functionality for: - Order placement - Order cancellation - Request authentication and signature generation """ def __init__(self, gateway: BinanceLinearGateway) -> None: """ The init method of the API. This method initializes the websocket client with a reference to the parent gateway. Parameters: gateway: the parent gateway object for pushing callback data """ super().__init__() self.gateway: BinanceLinearGateway = gateway self.gateway_name: str = gateway.gateway_name self.key: str = "" self.secret: bytes = b"" self.proxy_port: int = 0 self.proxy_host: str = "" self.server: str = "" self.reqid: int = 0 self.order_count: int = 0 self.order_prefix: str = "" self.reqid_callback_map: dict[int, Callable] = {} self.reqid_order_map: dict[int, OrderData] = {} def connect( self, key: str, secret: str, server: str, proxy_host: str, proxy_port: int ) -> None: """ Start server connection. This method initializes the API credentials and establishes a websocket connection to Binance trading API. Parameters: key: API Key for authentication secret: API Secret for request signing server: Server type ("REAL" or "TESTNET") proxy_host: Proxy server hostname or IP proxy_port: Proxy server port """ self.key = key self.secret = secret.encode() self.proxy_port = proxy_port self.proxy_host = proxy_host self.server = server self.order_prefix = datetime.now().strftime("%y%m%d%H%M%S") if self.server == "REAL": self.init(REAL_TRADE_HOST, proxy_host, proxy_port) else: self.init(TESTNET_TRADE_HOST, proxy_host, proxy_port) self.start() def sign(self, params: dict) -> None: """ Generate the signature for the request. This function creates an HMAC-SHA256 signature required for authenticated API requests to Binance. Parameters: params: Dictionary containing the parameters to be signed """ timestamp: int = int(time.time() * 1000) params["timestamp"] = timestamp payload: str = "&".join([f"{k}={v}" for k, v in sorted(params.items())]) signature: str = hmac.new( self.secret, payload.encode("utf-8"), hashlib.sha256 ).hexdigest() params["signature"] = signature def send_order(self, req: OrderRequest) -> str: """ Send new order to Binance. This function creates and sends a new order request to the exchange. It handles different order types including market, limit, and stop orders. Parameters: req: Order request object containing order details Returns: vt_orderid: The VeighNa order ID (gateway_name.orderid) if successful, empty string otherwise """ # Get contract contract: ContractData | None = self.gateway.get_contract_by_symbol(req.symbol) if not contract: self.gateway.write_log(f"Failed to send order, symbol not found: {req.symbol}") return "" # Generate new order id self.order_count += 1 orderid: str = self.order_prefix + str(self.order_count) # Push a submitting order event order: OrderData = req.create_order_data( orderid, self.gateway_name ) self.gateway.on_order(order) # Create order parameters params: dict = { "apiKey": self.key, "symbol": contract.name, "side": DIRECTION_VT2BINANCE[req.direction], "quantity": format_float(req.volume), "newClientOrderId": orderid, } if req.type == OrderType.MARKET: params["type"] = "MARKET" elif req.type == OrderType.STOP: params["type"] = "STOP_MARKET" params["stopPrice"] = format_float(req.price) else: order_type, time_condition = ORDERTYPE_VT2BINANCE[req.type] params["type"] = order_type params["timeInForce"] = time_condition params["price"] = format_float(req.price) self.sign(params) self.reqid += 1 self.reqid_callback_map[self.reqid] = self.on_send_order self.reqid_order_map[self.reqid] = order packet: dict = { "id": self.reqid, "method": "order.place", "params": params, } self.send_packet(packet) return order.vt_orderid def cancel_order(self, req: CancelRequest) -> None: """ Cancel existing order on Binance. This function sends a request to cancel an existing order on the exchange. Parameters: req: Cancel request object containing order details """ contract: ContractData | None = self.gateway.get_contract_by_symbol(req.symbol) if not contract: self.gateway.write_log(f"Failed to cancel order, symbol not found: {req.symbol}") return params: dict = { "apiKey": self.key, "symbol": contract.name, "origClientOrderId": req.orderid } self.sign(params) self.reqid += 1 self.reqid_callback_map[self.reqid] = self.on_cancel_order packet: dict = { "id": self.reqid, "method": "order.cancel", "params": params, } self.send_packet(packet) def on_connected(self) -> None: """ Callback when server is connected. This function is called when the trading websocket connection is successfully established. It logs the connection status. """ self.gateway.write_log("Trade API connected") def on_disconnected(self, status_code: int, msg: str) -> None: """ Callback when server is disconnected. This function is called when the trading websocket connection is closed. It logs the disconnection details. Parameters: status_code: HTTP status code for the disconnection msg: Disconnection message """ self.gateway.write_log(f"Trade API disconnected, code: {status_code}, msg: {msg}") def on_packet(self, packet: dict) -> None: """ Callback of data update. This function processes responses from the trading websocket API. It routes the response to the appropriate callback function based on the request ID. Parameters: packet: JSON data received from websocket """ reqid: int = packet.get("id", 0) callback: Callable | None = self.reqid_callback_map.get(reqid, None) if callback: callback(packet) def on_send_order(self, packet: dict) -> None: """ Callback of send order. This function processes the response to an order placement request. It handles errors by logging the details and updating the order status. Parameters: packet: JSON data received from websocket """ error: dict | None = packet.get("error", None) if not error: return error_code: str = error["code"] error_msg: str = error["msg"] msg: str = f"Order rejected, code: {error_code}, message: {error_msg}" self.gateway.write_log(msg) reqid: int = packet.get("id", 0) order: OrderData | None = self.reqid_order_map.get(reqid, None) if order: order.status = Status.REJECTED self.gateway.on_order(order) def on_cancel_order(self, packet: dict) -> None: """ Callback of cancel order. This function processes the response to an order cancellation request. It handles errors by logging the details. Parameters: packet: JSON data received from websocket """ error: dict | None = packet.get("error", None) if not error: return error_code: str = error["code"] error_msg: str = error["msg"] msg: str = f"Cancel rejected, code: {error_code}, message: {error_msg}" self.gateway.write_log(msg) def on_error(self, e: Exception) -> None: """ Callback when exception raised. This function is called when an exception occurs in the trading websocket connection. It logs the exception details for troubleshooting. Parameters: e: The exception that was raised """ self.gateway.write_log(f"Trade API exception: {e}") def generate_datetime(timestamp: float) -> datetime: """ Generate datetime object from Binance timestamp. This function converts a Binance millisecond timestamp to a datetime object with UTC timezone. Parameters: timestamp: Binance timestamp in milliseconds Returns: Datetime object with UTC timezone """ dt: datetime = datetime.fromtimestamp(timestamp / 1000, tz=UTC_TZ) return dt def format_float(f: float) -> str: """ Convert float number to string with correct precision. This function formats floating point numbers to avoid precision errors when sending requests to Binance. Parameters: f: The floating point number to format Returns: Formatted string representation of the number Note: Fixes potential error -1111: Parameter "quantity" has too much precision """ return format_float_positional(f, trim="-") ================================================ FILE: vnpy_binance/portfolio_gateway.py ================================================ """ Binance Portfolio Margin Gateway for VeighNa. This module provides trading functionality for Binance Portfolio Margin account, which supports unified account management across USDT-M futures, Coin-M futures, and cross margin trading. """ import hashlib import hmac import time import urllib.parse from copy import copy from enum import Enum from typing import Any from time import sleep from datetime import datetime, timedelta from numpy import format_float_positional from vnpy.event import Event, EventEngine from vnpy.trader.constant import ( Direction, Exchange, Product, Status, OrderType, Interval ) from vnpy.trader.gateway import BaseGateway from vnpy.trader.object import ( TickData, OrderData, TradeData, AccountData, ContractData, PositionData, BarData, OrderRequest, CancelRequest, SubscribeRequest, HistoryRequest ) from vnpy.trader.event import EVENT_TIMER from vnpy.trader.utility import round_to, ZoneInfo from vnpy_rest import Request, RestClient, Response from vnpy_websocket import WebsocketClient # Timezone constant UTC_TZ = ZoneInfo("UTC") # Real server hosts REAL_REST_HOST: str = "https://papi.binance.com" REAL_UM_REST_HOST: str = "https://fapi.binance.com" REAL_CM_REST_HOST: str = "https://dapi.binance.com" REAL_MARGIN_REST_HOST: str = "https://api.binance.com" REAL_USER_HOST: str = "wss://fstream.binance.com/pm/ws/" REAL_UM_PUBLIC_HOST: str = "wss://fstream.binance.com/public/stream" REAL_UM_MARKET_HOST: str = "wss://fstream.binance.com/market/stream" REAL_CM_DATA_HOST: str = "wss://dstream.binance.com/stream" REAL_MARGIN_DATA_HOST: str = "wss://stream.binance.com:9443/stream" # Testnet server hosts TESTNET_REST_HOST: str = "https://testnet.binancefuture.com" TESTNET_UM_REST_HOST: str = "https://testnet.binancefuture.com" TESTNET_CM_REST_HOST: str = "https://testnet.binancefuture.com" TESTNET_MARGIN_REST_HOST: str = "https://testnet.binance.vision" TESTNET_USER_HOST: str = "wss://stream.binancefuture.com/ws/" TESTNET_UM_PUBLIC_HOST: str = "wss://fstream.binancefuture.com/public/stream" TESTNET_UM_MARKET_HOST: str = "wss://fstream.binancefuture.com/market/stream" TESTNET_CM_DATA_HOST: str = "wss://dstream.binancefuture.com/stream" TESTNET_MARGIN_DATA_HOST: str = "wss://testnet.binance.vision/stream" # Order status map STATUS_BINANCE2VT: dict[str, Status] = { "NEW": Status.NOTTRADED, "PARTIALLY_FILLED": Status.PARTTRADED, "FILLED": Status.ALLTRADED, "CANCELED": Status.CANCELLED, "REJECTED": Status.REJECTED, "EXPIRED": Status.CANCELLED } # Order type map ORDERTYPE_VT2BINANCE: dict[OrderType, tuple[str, str]] = { OrderType.LIMIT: ("LIMIT", "GTC"), OrderType.MARKET: ("MARKET", "GTC"), OrderType.FAK: ("LIMIT", "IOC"), OrderType.FOK: ("LIMIT", "FOK"), } ORDERTYPE_BINANCE2VT: dict[tuple[str, str], OrderType] = { v: k for k, v in ORDERTYPE_VT2BINANCE.items() } # Direction map DIRECTION_VT2BINANCE: dict[Direction, str] = { Direction.LONG: "BUY", Direction.SHORT: "SELL" } DIRECTION_BINANCE2VT: dict[str, Direction] = { v: k for k, v in DIRECTION_VT2BINANCE.items() } # Product map for futures PRODUCT_BINANCE2VT: dict[str, Product] = { "PERPETUAL": Product.SWAP, "PERPETUAL_DELIVERING": Product.SWAP, "TRADIFI_PERPETUAL": Product.SWAP, "CURRENT_MONTH": Product.FUTURES, "NEXT_MONTH": Product.FUTURES, "CURRENT_QUARTER": Product.FUTURES, "NEXT_QUARTER": Product.FUTURES, } # Kline interval map INTERVAL_VT2BINANCE: dict[Interval, str] = { Interval.MINUTE: "1m", Interval.HOUR: "1h", Interval.DAILY: "1d", } # Timedelta map TIMEDELTA_MAP: dict[Interval, timedelta] = { Interval.MINUTE: timedelta(minutes=1), Interval.HOUR: timedelta(hours=1), Interval.DAILY: timedelta(days=1), } # Set websocket timeout to 24 hours WEBSOCKET_TIMEOUT = 24 * 60 * 60 class MarketType(Enum): """Market type for portfolio margin account""" UM = "um" # USDT-M Futures CM = "cm" # Coin-M Futures MARGIN = "margin" # Cross Margin def get_market_type(symbol: str) -> MarketType: """ Determine market type from symbol. Parameters: symbol: VeighNa symbol string Returns: MarketType enum value """ if "_SWAP_BINANCE" in symbol: base = symbol.replace("_SWAP_BINANCE", "") if base.endswith("USDT") or base.endswith("USDC"): return MarketType.UM else: return MarketType.CM elif "_SPOT_BINANCE" in symbol: return MarketType.MARGIN elif "_BINANCE" in symbol: # Delivery futures base = symbol.split("_")[0] if "USDT" in base or "USDC" in base: return MarketType.UM else: return MarketType.CM return MarketType.UM def generate_datetime(timestamp: float) -> datetime: """ Generate datetime object from Binance timestamp. Parameters: timestamp: Binance timestamp in milliseconds Returns: Datetime object with UTC timezone """ dt: datetime = datetime.fromtimestamp(timestamp / 1000, tz=UTC_TZ) return dt def format_float(f: float) -> str: """ Convert float number to string with correct precision. Parameters: f: The floating point number to format Returns: Formatted string representation of the number """ return format_float_positional(f, trim="-") class BinancePortfolioGateway(BaseGateway): """ The Binance portfolio margin trading gateway for VeighNa. This gateway provides unified trading functionality for Binance portfolio margin account, which supports USDT-M futures, Coin-M futures, and cross margin trading. Features: 1. Unified account management across all markets 2. Real-time market data with optional kline streaming 3. Only support crossed position and one-way mode """ default_name: str = "BINANCE_PORTFOLIO" default_setting: dict = { "API Key": "", "API Secret": "", "Server": ["REAL", "TESTNET"], "Kline Stream": ["False", "True"], "Proxy Host": "", "Proxy Port": 0 } exchanges: list[Exchange] = [Exchange.GLOBAL] def __init__(self, event_engine: EventEngine, gateway_name: str) -> None: """ The init method of the gateway. Parameters: event_engine: the global event engine object of VeighNa gateway_name: the unique name for identifying the gateway """ super().__init__(event_engine, gateway_name) self.user_api: UserApi = UserApi(self) self.um_md_api: UmMdApi = UmMdApi(self) self.cm_md_api: CmMdApi = CmMdApi(self) self.margin_md_api: MarginMdApi = MarginMdApi(self) self.rest_api: RestApi = RestApi(self) self.orders: dict[str, OrderData] = {} self.symbol_contract_map: dict[str, ContractData] = {} self.um_name_contract_map: dict[str, ContractData] = {} self.cm_name_contract_map: dict[str, ContractData] = {} self.margin_name_contract_map: dict[str, ContractData] = {} def connect(self, setting: dict) -> None: """ Start server connections. Parameters: setting: A dictionary containing connection parameters """ key: str = setting["API Key"] secret: str = setting["API Secret"] server: str = setting["Server"] kline_stream: bool = setting["Kline Stream"] == "True" proxy_host: str = setting["Proxy Host"] proxy_port: int = setting["Proxy Port"] self.rest_api.connect(key, secret, server, proxy_host, proxy_port) self.um_md_api.connect(server, kline_stream, proxy_host, proxy_port) self.cm_md_api.connect(server, kline_stream, proxy_host, proxy_port) self.margin_md_api.connect(server, kline_stream, proxy_host, proxy_port) self.event_engine.register(EVENT_TIMER, self.process_timer_event) def subscribe(self, req: SubscribeRequest) -> None: """ Subscribe to market data. Parameters: req: Subscription request object """ market_type: MarketType = get_market_type(req.symbol) if market_type == MarketType.UM: self.um_md_api.subscribe(req) elif market_type == MarketType.CM: self.cm_md_api.subscribe(req) else: self.margin_md_api.subscribe(req) def send_order(self, req: OrderRequest) -> str: """ Send new order. Parameters: req: Order request object Returns: str: The VeighNa order ID if successful, empty string if failed """ return self.rest_api.send_order(req) def cancel_order(self, req: CancelRequest) -> None: """ Cancel existing order. Parameters: req: Cancel request object """ self.rest_api.cancel_order(req) def query_account(self) -> None: """Query account balance.""" pass def query_position(self) -> None: """Query current positions.""" pass def query_history(self, req: HistoryRequest) -> list[BarData]: """ Query historical kline data. Parameters: req: History request object Returns: list[BarData]: List of historical kline data bars """ return self.rest_api.query_history(req) def close(self) -> None: """Close server connections.""" self.rest_api.stop() self.user_api.stop() self.um_md_api.stop() self.cm_md_api.stop() self.margin_md_api.stop() def process_timer_event(self, event: Event) -> None: """ Process timer task. Parameters: event: Timer event object """ self.rest_api.keep_user_stream() def on_order(self, order: OrderData) -> None: """ Save a copy of order and then push to event engine. Parameters: order: Order data object """ self.orders[order.orderid] = copy(order) super().on_order(order) def get_order(self, orderid: str) -> OrderData | None: """ Get previously saved order by order id. Parameters: orderid: The ID of the order to retrieve Returns: Order data object if found, None otherwise """ return self.orders.get(orderid) def on_contract(self, contract: ContractData) -> None: """ Save contract data in mappings and push to event engine. Parameters: contract: Contract data object """ self.symbol_contract_map[contract.symbol] = contract market_type: MarketType = get_market_type(contract.symbol) if market_type == MarketType.UM: self.um_name_contract_map[contract.name] = contract elif market_type == MarketType.CM: self.cm_name_contract_map[contract.name] = contract else: self.margin_name_contract_map[contract.name] = contract super().on_contract(contract) def get_contract_by_symbol(self, symbol: str) -> ContractData | None: """ Get contract data by VeighNa symbol. Parameters: symbol: VeighNa symbol Returns: Contract data object if found, None otherwise """ return self.symbol_contract_map.get(symbol, None) def get_contract_by_name(self, name: str, market_type: MarketType) -> ContractData | None: """ Get contract data by exchange symbol name and market type. Parameters: name: Exchange symbol name market_type: Market type for contract lookup Returns: Contract data object if found, None otherwise """ if market_type == MarketType.UM: return self.um_name_contract_map.get(name, None) elif market_type == MarketType.CM: return self.cm_name_contract_map.get(name, None) else: return self.margin_name_contract_map.get(name, None) def get_futures_contract_by_name(self, name: str) -> ContractData | None: """ Get futures contract data by exchange symbol name. Parameters: name: Exchange symbol name Returns: Contract data object if found, None otherwise """ contract: ContractData | None = self.um_name_contract_map.get(name, None) if contract: return contract return self.cm_name_contract_map.get(name, None) class RestApi(RestClient): """ The REST API of BinancePortfolioGateway. This class handles HTTP requests to Binance API endpoints, including: - Authentication and signature generation - Contract information queries for all markets - Account and position queries - Order management via Portfolio Margin endpoints - Historical data queries - User data stream management """ def __init__(self, gateway: BinancePortfolioGateway) -> None: """ The init method of the API. Parameters: gateway: the parent gateway object """ super().__init__() self.gateway: BinancePortfolioGateway = gateway self.gateway_name: str = gateway.gateway_name self.user_api: UserApi = self.gateway.user_api self.key: str = "" self.secret: bytes = b"" self.user_stream_key: str = "" self.keep_alive_count: int = 0 self.time_offset: int = 0 self.order_count: int = 1_000_000 self.order_prefix: str = "" # Additional REST clients for exchange info queries self.um_client: RestClient | None = None self.cm_client: RestClient | None = None self.margin_client: RestClient | None = None def sign(self, request: Request) -> Request: """ Standard callback for signing a request. Parameters: request: Request object to be signed Returns: Request: Modified request with authentication parameters """ if request.params: path: str = request.path + "?" + urllib.parse.urlencode(request.params) else: request.params = {} path = request.path timestamp: int = int(time.time() * 1000) if self.time_offset > 0: timestamp -= abs(self.time_offset) elif self.time_offset < 0: timestamp += abs(self.time_offset) request.params["timestamp"] = timestamp query: str = urllib.parse.urlencode(sorted(request.params.items())) signature: str = hmac.new( self.secret, query.encode("utf-8"), hashlib.sha256 ).hexdigest() query += f"&signature={signature}" path = request.path + "?" + query request.path = path request.params = {} request.data = {} request.headers = { "Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json", "X-MBX-APIKEY": self.key, "Connection": "close" } return request def connect( self, key: str, secret: str, server: str, proxy_host: str, proxy_port: int ) -> None: """Start server connection.""" self.key = key self.secret = secret.encode() self.proxy_port = proxy_port self.proxy_host = proxy_host self.server = server self.order_prefix = datetime.now().strftime("%y%m%d%H%M%S") # Initialize main REST client (Portfolio Margin) if self.server == "REAL": self.init(REAL_REST_HOST, proxy_host, proxy_port) else: self.init(TESTNET_REST_HOST, proxy_host, proxy_port) self.start() self.gateway.write_log("REST API started") # Initialize additional REST clients for exchange info self._init_market_clients(proxy_host, proxy_port) self.query_time() def _init_market_clients(self, proxy_host: str, proxy_port: int) -> None: """Initialize REST clients for each market's exchange info.""" if self.server == "REAL": um_host = REAL_UM_REST_HOST cm_host = REAL_CM_REST_HOST margin_host = REAL_MARGIN_REST_HOST else: um_host = TESTNET_UM_REST_HOST cm_host = TESTNET_CM_REST_HOST margin_host = TESTNET_MARGIN_REST_HOST # UM client self.um_client = RestClient() self.um_client.init(um_host, proxy_host, proxy_port) self.um_client.start() # CM client self.cm_client = RestClient() self.cm_client.init(cm_host, proxy_host, proxy_port) self.cm_client.start() # Margin client self.margin_client = RestClient() self.margin_client.init(margin_host, proxy_host, proxy_port) self.margin_client.start() def query_time(self) -> None: """Query server time to calculate local time offset.""" path: str = "/papi/v1/time" self.add_request( "GET", path, callback=self.on_query_time ) def query_account(self) -> None: """Query account balance for all markets.""" # Query UM account path: str = "/papi/v1/um/account" self.add_request( method="GET", path=path, callback=self.on_query_um_account, ) # Query CM account path = "/papi/v1/cm/account" self.add_request( method="GET", path=path, callback=self.on_query_cm_account, ) # Query margin balance path = "/papi/v1/balance" self.add_request( method="GET", path=path, callback=self.on_query_margin_account, ) def query_position(self) -> None: """Query holding positions for futures markets.""" # Query UM positions path: str = "/papi/v1/um/positionRisk" self.add_request( method="GET", path=path, callback=self.on_query_um_position, ) # Query CM positions path = "/papi/v1/cm/positionRisk" self.add_request( method="GET", path=path, callback=self.on_query_cm_position, ) def query_order(self) -> None: """Query open orders for all markets.""" # Query UM orders path: str = "/papi/v1/um/openOrders" self.add_request( method="GET", path=path, callback=self.on_query_um_order, ) # Query CM orders path = "/papi/v1/cm/openOrders" self.add_request( method="GET", path=path, callback=self.on_query_cm_order, ) # Query margin orders path = "/papi/v1/margin/openOrders" self.add_request( method="GET", path=path, callback=self.on_query_margin_order, ) def query_contract(self) -> None: """Query available contracts for all markets.""" # Query UM contracts via fapi if self.um_client: resp: Response = self.um_client.request( "GET", "/fapi/v1/exchangeInfo" ) if resp.status_code == 200: self.on_query_um_contract(resp.json()) else: self.gateway.write_log(f"Query UM contract failed: {resp.text}") # Query CM contracts via dapi if self.cm_client: resp = self.cm_client.request( "GET", "/dapi/v1/exchangeInfo" ) if resp.status_code == 200: self.on_query_cm_contract(resp.json()) else: self.gateway.write_log(f"Query CM contract failed: {resp.text}") # Query margin contracts via spot api if self.margin_client: resp = self.margin_client.request( "GET", "/api/v3/exchangeInfo" ) if resp.status_code == 200: self.on_query_margin_contract(resp.json()) else: self.gateway.write_log(f"Query margin contract failed: {resp.text}") def send_order(self, req: OrderRequest) -> str: """ Send new order via REST API. Parameters: req: Order request object Returns: vt_orderid: The VeighNa order ID if successful, empty string otherwise """ contract: ContractData | None = self.gateway.get_contract_by_symbol(req.symbol) if not contract: self.gateway.write_log(f"Failed to send order, symbol not found: {req.symbol}") return "" # Generate new order id self.order_count += 1 orderid: str = self.order_prefix + str(self.order_count) # Push a submitting order event order: OrderData = req.create_order_data( orderid, self.gateway_name ) self.gateway.on_order(order) # Create order parameters params: dict = { "symbol": contract.name, "side": DIRECTION_VT2BINANCE[req.direction], "quantity": format_float(req.volume), "newClientOrderId": orderid, } if req.type == OrderType.MARKET: params["type"] = "MARKET" else: order_type, time_condition = ORDERTYPE_VT2BINANCE[req.type] params["type"] = order_type params["timeInForce"] = time_condition params["price"] = format_float(req.price) # Select endpoint based on market type market_type: MarketType = get_market_type(req.symbol) if market_type == MarketType.UM: path = "/papi/v1/um/order" elif market_type == MarketType.CM: path = "/papi/v1/cm/order" else: path = "/papi/v1/margin/order" self.add_request( method="POST", path=path, callback=self.on_send_order, params=params, extra=order, on_failed=self.on_send_order_failed, on_error=self.on_send_order_error ) return order.vt_orderid def cancel_order(self, req: CancelRequest) -> None: """ Cancel existing order. Parameters: req: Cancel request object """ order: OrderData | None = self.gateway.get_order(req.orderid) if not order: self.gateway.write_log(f"Failed to cancel order, order not found: {req.orderid}") return contract: ContractData | None = self.gateway.get_contract_by_symbol(order.symbol) if not contract: self.gateway.write_log(f"Failed to cancel order, symbol not found: {order.symbol}") return params: dict = { "symbol": contract.name, "origClientOrderId": req.orderid } # Select endpoint based on market type market_type: MarketType = get_market_type(order.symbol) if market_type == MarketType.UM: path = "/papi/v1/um/order" elif market_type == MarketType.CM: path = "/papi/v1/cm/order" else: path = "/papi/v1/margin/order" self.add_request( method="DELETE", path=path, callback=self.on_cancel_order, params=params, extra=order, on_failed=self.on_cancel_order_failed ) def start_user_stream(self) -> None: """Create listen key for user stream.""" path: str = "/papi/v1/listenKey" self.add_request( method="POST", path=path, callback=self.on_start_user_stream, ) def keep_user_stream(self) -> None: """Extend listen key validity.""" if not self.user_stream_key: return self.keep_alive_count += 1 if self.keep_alive_count < 600: return self.keep_alive_count = 0 params: dict = {"listenKey": self.user_stream_key} path: str = "/papi/v1/listenKey" self.add_request( method="PUT", path=path, callback=self.on_keep_user_stream, params=params, on_error=self.on_keep_user_stream_error ) def on_query_time(self, data: dict, request: Request) -> None: """Callback of server time query.""" local_time: int = int(time.time() * 1000) server_time: int = int(data["serverTime"]) self.time_offset = local_time - server_time self.gateway.write_log(f"Server time updated, local offset: {self.time_offset}ms") # Query contracts after time sync self.query_contract() # Query private data if authenticated if self.key and self.secret: self.query_order() self.query_account() self.query_position() self.start_user_stream() def on_query_um_account(self, data: dict, request: Request) -> None: """Callback of UM account balance query.""" for asset in data["assets"]: account: AccountData = AccountData( accountid=asset["asset"] + "_UM", balance=float(asset["crossWalletBalance"]), frozen=float(asset["maintMargin"]), gateway_name=self.gateway_name ) if account.balance: self.gateway.on_account(account) self.gateway.write_log("UM account data received") def on_query_cm_account(self, data: dict, request: Request) -> None: """Callback of CM account balance query.""" for asset in data["assets"]: account: AccountData = AccountData( accountid=asset["asset"] + "_CM", balance=float(asset["crossWalletBalance"]), frozen=float(asset["maintMargin"]), gateway_name=self.gateway_name ) if account.balance: self.gateway.on_account(account) self.gateway.write_log("CM account data received") def on_query_margin_account(self, data: list, request: Request) -> None: """Callback of margin account balance query.""" for asset in data: account: AccountData = AccountData( accountid=asset["asset"] + "_MARGIN", balance=float(asset["crossMarginAsset"]), frozen=float(asset["crossMarginLocked"]), gateway_name=self.gateway_name ) if account.balance: self.gateway.on_account(account) self.gateway.write_log("Margin account data received") def on_query_um_position(self, data: list, request: Request) -> None: """Callback of UM positions query.""" for d in data: name: str = d["symbol"] contract: ContractData | None = self.gateway.get_contract_by_name(name, MarketType.UM) if not contract: continue volume_str = d["positionAmt"] if "." in volume_str: volume = float(volume_str) else: volume = int(volume_str) position: PositionData = PositionData( symbol=contract.symbol, exchange=Exchange.GLOBAL, direction=Direction.NET, volume=volume, price=float(d["entryPrice"]), pnl=float(d["unRealizedProfit"]), gateway_name=self.gateway_name, ) if position.volume: self.gateway.on_position(position) self.gateway.write_log("UM position data received") def on_query_cm_position(self, data: list, request: Request) -> None: """Callback of CM positions query.""" for d in data: name: str = d["symbol"] contract: ContractData | None = self.gateway.get_contract_by_name(name, MarketType.CM) if not contract: continue volume_str = d["positionAmt"] if "." in volume_str: volume = float(volume_str) else: volume = int(volume_str) position: PositionData = PositionData( symbol=contract.symbol, exchange=Exchange.GLOBAL, direction=Direction.NET, volume=volume, price=float(d["entryPrice"]), pnl=float(d["unRealizedProfit"]), gateway_name=self.gateway_name, ) if position.volume: self.gateway.on_position(position) self.gateway.write_log("CM position data received") def on_query_um_order(self, data: list, request: Request) -> None: """Callback of UM open orders query.""" for d in data: key: tuple[str, str] = (d["type"], d["timeInForce"]) order_type: OrderType | None = ORDERTYPE_BINANCE2VT.get(key, None) if not order_type: continue contract: ContractData | None = self.gateway.get_contract_by_name(d["symbol"], MarketType.UM) if not contract: continue order: OrderData = OrderData( orderid=d["clientOrderId"], symbol=contract.symbol, exchange=Exchange.GLOBAL, price=float(d["price"]), volume=float(d["origQty"]), type=order_type, direction=DIRECTION_BINANCE2VT[d["side"]], traded=float(d["executedQty"]), status=STATUS_BINANCE2VT.get(d["status"], Status.SUBMITTING), datetime=generate_datetime(d["time"]), gateway_name=self.gateway_name, ) self.gateway.on_order(order) self.gateway.write_log("UM order data received") def on_query_cm_order(self, data: list, request: Request) -> None: """Callback of CM open orders query.""" for d in data: key: tuple[str, str] = (d["type"], d["timeInForce"]) order_type: OrderType | None = ORDERTYPE_BINANCE2VT.get(key, None) if not order_type: continue contract: ContractData | None = self.gateway.get_contract_by_name(d["symbol"], MarketType.CM) if not contract: continue order: OrderData = OrderData( orderid=d["clientOrderId"], symbol=contract.symbol, exchange=Exchange.GLOBAL, price=float(d["price"]), volume=float(d["origQty"]), type=order_type, direction=DIRECTION_BINANCE2VT[d["side"]], traded=float(d["executedQty"]), status=STATUS_BINANCE2VT.get(d["status"], Status.SUBMITTING), datetime=generate_datetime(d["time"]), gateway_name=self.gateway_name, ) self.gateway.on_order(order) self.gateway.write_log("CM order data received") def on_query_margin_order(self, data: list, request: Request) -> None: """Callback of margin open orders query.""" for d in data: key: tuple[str, str] = (d["type"], d["timeInForce"]) order_type: OrderType | None = ORDERTYPE_BINANCE2VT.get(key, None) if not order_type: continue contract: ContractData | None = self.gateway.get_contract_by_name(d["symbol"], MarketType.MARGIN) if not contract: continue order: OrderData = OrderData( orderid=d["clientOrderId"], symbol=contract.symbol, exchange=Exchange.GLOBAL, price=float(d["price"]), volume=float(d["origQty"]), type=order_type, direction=DIRECTION_BINANCE2VT[d["side"]], traded=float(d["executedQty"]), status=STATUS_BINANCE2VT.get(d["status"], Status.SUBMITTING), datetime=generate_datetime(d["time"]), gateway_name=self.gateway_name, ) self.gateway.on_order(order) self.gateway.write_log("Margin order data received") def on_query_um_contract(self, data: dict) -> None: """Callback of UM contracts query.""" for d in data["symbols"]: pricetick: float = 1 min_volume: float = 1 max_volume: float = 1 for f in d["filters"]: if f["filterType"] == "PRICE_FILTER": pricetick = float(f["tickSize"]) elif f["filterType"] == "LOT_SIZE": min_volume = float(f["minQty"]) max_volume = float(f["maxQty"]) product: Product | None = PRODUCT_BINANCE2VT.get(d["contractType"], None) if product == Product.SWAP: symbol = d["symbol"] + "_SWAP_BINANCE" elif product == Product.FUTURES: symbol = d["symbol"] + "_BINANCE" else: continue contract: ContractData = ContractData( symbol=symbol, exchange=Exchange.GLOBAL, name=d["symbol"], pricetick=pricetick, size=1, min_volume=min_volume, max_volume=max_volume, product=product, net_position=True, history_data=True, gateway_name=self.gateway_name, stop_supported=False ) self.gateway.on_contract(contract) self.gateway.write_log("UM contract data received") def on_query_cm_contract(self, data: dict) -> None: """Callback of CM contracts query.""" for d in data["symbols"]: pricetick: float = 1 min_volume: float = 1 max_volume: float = 1 for f in d["filters"]: if f["filterType"] == "PRICE_FILTER": pricetick = float(f["tickSize"]) elif f["filterType"] == "LOT_SIZE": min_volume = float(f["minQty"]) max_volume = float(f["maxQty"]) product: Product | None = PRODUCT_BINANCE2VT.get(d["contractType"], None) if product == Product.SWAP: symbol = d["symbol"].replace("_PERP", "") + "_SWAP_BINANCE" elif product == Product.FUTURES: symbol = d["symbol"] + "_BINANCE" else: continue contract: ContractData = ContractData( symbol=symbol, exchange=Exchange.GLOBAL, name=d["symbol"], pricetick=pricetick, size=1, min_volume=min_volume, max_volume=max_volume, product=product, net_position=True, history_data=True, gateway_name=self.gateway_name, stop_supported=False ) self.gateway.on_contract(contract) self.gateway.write_log("CM contract data received") def on_query_margin_contract(self, data: dict) -> None: """Callback of margin contracts query.""" for d in data["symbols"]: pricetick: float = 1 min_volume: float = 1 max_volume: float = 1 for f in d["filters"]: if f["filterType"] == "PRICE_FILTER": pricetick = float(f["tickSize"]) elif f["filterType"] == "LOT_SIZE": min_volume = float(f["minQty"]) max_volume = float(f["maxQty"]) symbol = d["symbol"] + "_SPOT_BINANCE" contract: ContractData = ContractData( symbol=symbol, exchange=Exchange.GLOBAL, name=d["symbol"], pricetick=pricetick, size=1, min_volume=min_volume, max_volume=max_volume, product=Product.SPOT, net_position=True, history_data=True, gateway_name=self.gateway_name, stop_supported=False ) self.gateway.on_contract(contract) self.gateway.write_log("Margin contract data received") def on_send_order(self, data: dict, request: Request) -> None: """Callback of send order.""" pass def on_send_order_failed(self, status_code: int, request: Request) -> None: """Callback when send order failed.""" order: OrderData = request.extra order.status = Status.REJECTED self.gateway.on_order(order) data: dict = request.response.json() error_code: int = data["code"] error_msg: str = data["msg"] msg: str = f"Order failed, code: {error_code}, message: {error_msg}" self.gateway.write_log(msg) def on_send_order_error( self, exception_type: type, exception_value: Exception, tb: Any, request: Request ) -> None: """Callback when send order has error.""" order: OrderData = request.extra order.status = Status.REJECTED self.gateway.on_order(order) if not issubclass(exception_type, ConnectionError | TimeoutError): self.on_error(exception_type, exception_value, tb, request) def on_cancel_order(self, data: dict, request: Request) -> None: """Callback of cancel order.""" pass def on_cancel_order_failed(self, status_code: int, request: Request) -> None: """Callback when cancel order failed.""" data: dict = request.response.json() error_code: int = data["code"] error_msg: str = data["msg"] msg: str = f"Cancel failed, code: {error_code}, message: {error_msg}" self.gateway.write_log(msg) def on_start_user_stream(self, data: dict, request: Request) -> None: """Callback of start user stream.""" self.user_stream_key = data["listenKey"] self.keep_alive_count = 0 if self.server == "REAL": url = REAL_USER_HOST + self.user_stream_key else: url = TESTNET_USER_HOST + self.user_stream_key self.user_api.connect(url, self.proxy_host, self.proxy_port) def on_keep_user_stream(self, data: dict, request: Request) -> None: """Callback of keep user stream.""" pass def on_keep_user_stream_error( self, exception_type: type, exception_value: Exception, tb: Any, request: Request ) -> None: """Error callback of keep user stream.""" if not issubclass(exception_type, TimeoutError): self.on_error(exception_type, exception_value, tb, request) def query_history(self, req: HistoryRequest) -> list[BarData]: """Query kline history data.""" contract: ContractData | None = self.gateway.get_contract_by_symbol(req.symbol) if not contract: return [] # Check interval if not req.interval: return [] history: list[BarData] = [] limit: int = 1500 start_time: int = int(datetime.timestamp(req.start)) # Select endpoint based on market type market_type: MarketType = get_market_type(req.symbol) if market_type == MarketType.UM: client = self.um_client path = "/fapi/v1/klines" elif market_type == MarketType.CM: client = self.cm_client path = "/dapi/v1/klines" else: client = self.margin_client path = "/api/v3/klines" limit = 1000 if not client: return [] while True: params: dict = { "symbol": contract.name, "interval": INTERVAL_VT2BINANCE[req.interval], "limit": limit, "startTime": start_time * 1000 } if req.end: end_time = int(datetime.timestamp(req.end)) params["endTime"] = end_time * 1000 resp: Response = client.request( "GET", path=path, params=params ) if resp.status_code // 100 != 2: msg: str = f"Query kline history failed, status code: {resp.status_code}, message: {resp.text}" self.gateway.write_log(msg) break else: data: list = resp.json() if not data: msg = f"No kline history data received, start time: {start_time}" self.gateway.write_log(msg) break buf: list[BarData] = [] for row in data: bar: BarData = BarData( symbol=req.symbol, exchange=req.exchange, datetime=generate_datetime(row[0]), interval=req.interval, volume=float(row[5]), turnover=float(row[7]), open_price=float(row[1]), high_price=float(row[2]), low_price=float(row[3]), close_price=float(row[4]), gateway_name=self.gateway_name ) buf.append(bar) begin: datetime = buf[0].datetime end: datetime = buf[-1].datetime history.extend(buf) msg = f"Query kline history finished, {req.symbol} - {req.interval.value}, {begin} - {end}" self.gateway.write_log(msg) next_start_dt = bar.datetime + TIMEDELTA_MAP[req.interval] next_start_time = int(datetime.timestamp(next_start_dt)) if len(data) < limit or (req.end and next_start_dt >= req.end): break start_time = next_start_time sleep(0.5) if history: history.pop(-1) return history def stop(self) -> None: """Stop REST API and cleanup.""" super().stop() if self.um_client: self.um_client.stop() if self.cm_client: self.cm_client.stop() if self.margin_client: self.margin_client.stop() class UserApi(WebsocketClient): """ The user data websocket API of BinancePortfolioGateway. Handles real-time updates for: - Account balance changes - Position updates - Order status changes - Trade executions """ def __init__(self, gateway: BinancePortfolioGateway) -> None: """Initialize the API.""" super().__init__() self.gateway: BinancePortfolioGateway = gateway self.gateway_name: str = gateway.gateway_name def connect(self, url: str, proxy_host: str, proxy_port: int) -> None: """Start server connection.""" self.init(url, proxy_host, proxy_port, receive_timeout=WEBSOCKET_TIMEOUT) self.start() def on_connected(self) -> None: """Callback when server is connected.""" self.gateway.write_log("User API connected") def on_packet(self, packet: dict) -> None: """Callback of data update.""" match packet["e"]: case "ACCOUNT_UPDATE": self.on_account(packet) case "ORDER_TRADE_UPDATE": self.on_order(packet) case "outboundAccountPosition": self.on_account_outbound(packet) case "executionReport": self.on_execution_report(packet) case "listenKeyExpired": self.on_listen_key_expired() def on_listen_key_expired(self) -> None: """Callback of listen key expired.""" self.gateway.write_log("Listen key expired") self.disconnect() def on_account(self, packet: dict) -> None: """Callback of futures account update.""" for acc_data in packet["a"]["B"]: account: AccountData = AccountData( accountid=acc_data["a"], balance=float(acc_data["wb"]), frozen=float(acc_data["wb"]) - float(acc_data["cw"]), gateway_name=self.gateway_name ) if account.balance: self.gateway.on_account(account) for pos_data in packet["a"]["P"]: if pos_data["ps"] == "BOTH": volume = pos_data["pa"] if "." in volume: volume = float(volume) else: volume = int(volume) name: str = pos_data["s"] contract: ContractData | None = self.gateway.get_futures_contract_by_name(name) if not contract: continue position: PositionData = PositionData( symbol=contract.symbol, exchange=Exchange.GLOBAL, direction=Direction.NET, volume=volume, price=float(pos_data["ep"]), pnl=float(pos_data["up"]), gateway_name=self.gateway_name, ) self.gateway.on_position(position) def on_account_outbound(self, packet: dict) -> None: """Callback of margin account balance update.""" for acc_data in packet["B"]: free = float(acc_data["f"]) locked = float(acc_data["l"]) if free or locked: account: AccountData = AccountData( accountid=acc_data["a"] + "_MARGIN", balance=free + locked, frozen=locked, gateway_name=self.gateway_name ) self.gateway.on_account(account) def on_order(self, packet: dict) -> None: """Callback of futures order update.""" ord_data: dict = packet["o"] key: tuple[str, str] = (ord_data["o"], ord_data["f"]) order_type: OrderType | None = ORDERTYPE_BINANCE2VT.get(key, None) if not order_type: return name: str = ord_data["s"] contract: ContractData | None = self.gateway.get_futures_contract_by_name(name) if not contract: return order: OrderData = OrderData( symbol=contract.symbol, exchange=Exchange.GLOBAL, orderid=str(ord_data["c"]), type=order_type, direction=DIRECTION_BINANCE2VT[ord_data["S"]], price=float(ord_data["p"]), volume=float(ord_data["q"]), traded=float(ord_data["z"]), status=STATUS_BINANCE2VT[ord_data["X"]], datetime=generate_datetime(packet["E"]), gateway_name=self.gateway_name, ) self.gateway.on_order(order) trade_volume: float = float(ord_data["l"]) trade_volume = round_to(trade_volume, contract.min_volume) if not trade_volume: return trade: TradeData = TradeData( symbol=order.symbol, exchange=order.exchange, orderid=order.orderid, tradeid=ord_data["t"], direction=order.direction, price=float(ord_data["L"]), volume=trade_volume, datetime=generate_datetime(ord_data["T"]), gateway_name=self.gateway_name, ) self.gateway.on_trade(trade) def on_execution_report(self, packet: dict) -> None: """Callback of margin order update.""" key: tuple[str, str] = (packet["o"], packet["f"]) order_type: OrderType | None = ORDERTYPE_BINANCE2VT.get(key, None) if not order_type: return name: str = packet["s"] contract: ContractData | None = self.gateway.get_contract_by_name(name, MarketType.MARGIN) if not contract: return # Handle cancel order if packet["x"] == "CANCELED": orderid = packet["C"] else: orderid = packet["c"] order: OrderData = OrderData( symbol=contract.symbol, exchange=Exchange.GLOBAL, orderid=str(orderid), type=order_type, direction=DIRECTION_BINANCE2VT[packet["S"]], price=float(packet["p"]), volume=float(packet["q"]), traded=float(packet["z"]), status=STATUS_BINANCE2VT[packet["X"]], datetime=generate_datetime(packet["E"]), gateway_name=self.gateway_name, ) self.gateway.on_order(order) trade_volume: float = float(packet["l"]) trade_volume = round_to(trade_volume, contract.min_volume) if not trade_volume: return trade: TradeData = TradeData( symbol=order.symbol, exchange=order.exchange, orderid=order.orderid, tradeid=packet["t"], direction=order.direction, price=float(packet["L"]), volume=trade_volume, datetime=generate_datetime(packet["T"]), gateway_name=self.gateway_name, ) self.gateway.on_trade(trade) def on_disconnected(self, status_code: int, msg: str) -> None: """Callback when server is disconnected.""" self.gateway.write_log(f"User API disconnected, code: {status_code}, msg: {msg}") self.gateway.rest_api.start_user_stream() def on_error(self, e: Exception) -> None: """Callback when exception raised.""" self.gateway.write_log(f"User API exception: {e}") class UmMdApi(WebsocketClient): """ The USDT-M futures market data websocket API. Handles real-time updates for: - Tickers (24hr statistics) - Order book depth (10 levels) - Klines (candlestick data) if enabled """ def __init__(self, gateway: BinancePortfolioGateway) -> None: """Initialize the API.""" super().__init__() self.gateway: BinancePortfolioGateway = gateway self.gateway_name: str = gateway.gateway_name self.ticks: dict[str, TickData] = {} self.public_api: UmPublicMdApi = UmPublicMdApi(self) self.reqid: int = 0 self.kline_stream: bool = False def connect( self, server: str, kline_stream: bool, proxy_host: str, proxy_port: int, ) -> None: """Start server connection.""" self.kline_stream = kline_stream if server == "REAL": self.init(REAL_UM_MARKET_HOST, proxy_host, proxy_port, receive_timeout=WEBSOCKET_TIMEOUT) self.public_api.connect(REAL_UM_PUBLIC_HOST, proxy_host, proxy_port) else: self.init(TESTNET_UM_MARKET_HOST, proxy_host, proxy_port, receive_timeout=WEBSOCKET_TIMEOUT) self.public_api.connect(TESTNET_UM_PUBLIC_HOST, proxy_host, proxy_port) self.start() def stop(self) -> None: """Stop market data websocket connections.""" self.public_api.stop() super().stop() def get_public_channels(self, contract: ContractData) -> list[str]: """Generate public market data channels for a contract.""" return [f"{contract.name.lower()}@depth10"] def get_market_channels(self, contract: ContractData) -> list[str]: """Generate market data channels for a contract.""" channels: list[str] = [f"{contract.name.lower()}@ticker"] if self.kline_stream: channels.append(f"{contract.name.lower()}@kline_1m") return channels def send_subscribe_packet(self, api: WebsocketClient, channels: list[str]) -> None: """Send a subscribe packet through the given websocket client.""" if not channels: return self.reqid += 1 packet: dict = { "method": "SUBSCRIBE", "params": channels, "id": self.reqid } api.send_packet(packet) def resubscribe_market_channels(self) -> None: """Resubscribe market channels after reconnect.""" channels: list[str] = [] for symbol in self.ticks.keys(): contract: ContractData | None = self.gateway.get_contract_by_symbol(symbol) if not contract: continue channels.extend(self.get_market_channels(contract)) self.send_subscribe_packet(self, channels) def resubscribe_public_channels(self) -> None: """Resubscribe public channels after reconnect.""" channels: list[str] = [] for symbol in self.ticks.keys(): contract: ContractData | None = self.gateway.get_contract_by_symbol(symbol) if not contract: continue channels.extend(self.get_public_channels(contract)) self.send_subscribe_packet(self.public_api, channels) def on_connected(self) -> None: """Callback when server is connected.""" self.gateway.write_log("UM MD API connected") if self.ticks: self.resubscribe_market_channels() def subscribe(self, req: SubscribeRequest) -> None: """Subscribe to market data.""" if req.symbol in self.ticks: return contract: ContractData | None = self.gateway.get_contract_by_symbol(req.symbol) if not contract: self.gateway.write_log(f"Failed to subscribe data, symbol not found: {req.symbol}") return tick: TickData = TickData( symbol=req.symbol, name=contract.name, exchange=Exchange.GLOBAL, datetime=datetime.now(UTC_TZ), gateway_name=self.gateway_name, ) tick.extra = {} self.ticks[req.symbol] = tick self.send_subscribe_packet(self, self.get_market_channels(contract)) self.send_subscribe_packet(self.public_api, self.get_public_channels(contract)) def on_packet(self, packet: dict) -> None: """Callback of market data update.""" stream: str = packet.get("stream", "") if not stream: return data: dict = packet["data"] name, channel = stream.split("@", 1) contract: ContractData | None = self.gateway.get_contract_by_name(name.upper(), MarketType.UM) if not contract: return tick: TickData | None = self.ticks.get(contract.symbol) if not tick: return if channel == "ticker": tick.volume = float(data["v"]) tick.turnover = float(data["q"]) tick.open_price = float(data["o"]) tick.high_price = float(data["h"]) tick.low_price = float(data["l"]) tick.last_price = float(data["c"]) tick.datetime = generate_datetime(float(data["E"])) elif channel == "depth10": bids: list = data["b"] for n in range(min(10, len(bids))): price, volume = bids[n] tick.__setattr__("bid_price_" + str(n + 1), float(price)) tick.__setattr__("bid_volume_" + str(n + 1), float(volume)) asks: list = data["a"] for n in range(min(10, len(asks))): price, volume = asks[n] tick.__setattr__("ask_price_" + str(n + 1), float(price)) tick.__setattr__("ask_volume_" + str(n + 1), float(volume)) tick.datetime = generate_datetime(float(data["E"])) else: kline_data: dict = data["k"] bar_ready: bool = kline_data.get("x", False) if not bar_ready: return dt: datetime = generate_datetime(float(kline_data["t"])) if tick.extra is None: tick.extra = {} tick.extra["bar"] = BarData( symbol=contract.symbol, exchange=Exchange.GLOBAL, datetime=dt.replace(second=0, microsecond=0), interval=Interval.MINUTE, volume=float(kline_data["v"]), turnover=float(kline_data["q"]), open_price=float(kline_data["o"]), high_price=float(kline_data["h"]), low_price=float(kline_data["l"]), close_price=float(kline_data["c"]), gateway_name=self.gateway_name ) if tick.last_price: tick.localtime = datetime.now() self.gateway.on_tick(copy(tick)) def on_disconnected(self, status_code: int, msg: str) -> None: """Callback when server is disconnected.""" self.gateway.write_log(f"UM MD API disconnected, code: {status_code}, msg: {msg}") def on_error(self, e: Exception) -> None: """Callback when exception raised.""" self.gateway.write_log(f"UM MD API exception: {e}") class UmPublicMdApi(WebsocketClient): """Public market data websocket connection for UM high-frequency streams.""" def __init__(self, md_api: UmMdApi) -> None: """Initialize the API.""" super().__init__() self.md_api: UmMdApi = md_api self.gateway: BinancePortfolioGateway = md_api.gateway def connect(self, url: str, proxy_host: str, proxy_port: int) -> None: """Start public market data websocket connection.""" self.init(url, proxy_host, proxy_port, receive_timeout=WEBSOCKET_TIMEOUT) self.start() def on_connected(self) -> None: """Callback when public websocket is connected.""" self.gateway.write_log("UM Public API connected") if self.md_api.ticks: self.md_api.resubscribe_public_channels() def on_packet(self, packet: dict) -> None: """Forward packets to the shared UM market data parser.""" self.md_api.on_packet(packet) def on_disconnected(self, status_code: int, msg: str) -> None: """Callback when public websocket is disconnected.""" self.gateway.write_log(f"UM Public API disconnected, code: {status_code}, msg: {msg}") def on_error(self, e: Exception) -> None: """Callback when public websocket raises an exception.""" self.gateway.write_log(f"UM Public API exception: {e}") class CmMdApi(WebsocketClient): """ The Coin-M futures market data websocket API. Handles real-time updates for: - Tickers (24hr statistics) - Order book depth (10 levels) - Klines (candlestick data) if enabled """ def __init__(self, gateway: BinancePortfolioGateway) -> None: """Initialize the API.""" super().__init__() self.gateway: BinancePortfolioGateway = gateway self.gateway_name: str = gateway.gateway_name self.ticks: dict[str, TickData] = {} self.reqid: int = 0 self.kline_stream: bool = False def connect( self, server: str, kline_stream: bool, proxy_host: str, proxy_port: int, ) -> None: """Start server connection.""" self.kline_stream = kline_stream if server == "REAL": self.init(REAL_CM_DATA_HOST, proxy_host, proxy_port, receive_timeout=WEBSOCKET_TIMEOUT) else: self.init(TESTNET_CM_DATA_HOST, proxy_host, proxy_port, receive_timeout=WEBSOCKET_TIMEOUT) self.start() def on_connected(self) -> None: """Callback when server is connected.""" self.gateway.write_log("CM MD API connected") if self.ticks: channels = [] for symbol in self.ticks.keys(): channels.append(f"{symbol}@ticker") channels.append(f"{symbol}@depth10") if self.kline_stream: channels.append(f"{symbol}@kline_1m") packet: dict = { "method": "SUBSCRIBE", "params": channels, "id": self.reqid } self.send_packet(packet) def subscribe(self, req: SubscribeRequest) -> None: """Subscribe to market data.""" if req.symbol in self.ticks: return contract: ContractData | None = self.gateway.get_contract_by_symbol(req.symbol) if not contract: self.gateway.write_log(f"Failed to subscribe data, symbol not found: {req.symbol}") return self.reqid += 1 tick: TickData = TickData( symbol=req.symbol, name=contract.name, exchange=Exchange.GLOBAL, datetime=datetime.now(UTC_TZ), gateway_name=self.gateway_name, ) tick.extra = {} self.ticks[req.symbol] = tick channels: list[str] = [ f"{contract.name.lower()}@ticker", f"{contract.name.lower()}@depth10" ] if self.kline_stream: channels.append(f"{contract.name.lower()}@kline_1m") packet: dict = { "method": "SUBSCRIBE", "params": channels, "id": self.reqid } self.send_packet(packet) def on_packet(self, packet: dict) -> None: """Callback of market data update.""" stream: str = packet.get("stream", "") if not stream: return data: dict = packet["data"] name, channel = stream.split("@") contract: ContractData | None = self.gateway.get_contract_by_name(name.upper(), MarketType.CM) if not contract: return tick: TickData | None = self.ticks.get(contract.symbol) if not tick: return if channel == "ticker": tick.volume = float(data["v"]) tick.turnover = float(data["q"]) tick.open_price = float(data["o"]) tick.high_price = float(data["h"]) tick.low_price = float(data["l"]) tick.last_price = float(data["c"]) tick.datetime = generate_datetime(float(data["E"])) elif channel == "depth10": bids: list = data["b"] for n in range(min(10, len(bids))): price, volume = bids[n] tick.__setattr__("bid_price_" + str(n + 1), float(price)) tick.__setattr__("bid_volume_" + str(n + 1), float(volume)) asks: list = data["a"] for n in range(min(10, len(asks))): price, volume = asks[n] tick.__setattr__("ask_price_" + str(n + 1), float(price)) tick.__setattr__("ask_volume_" + str(n + 1), float(volume)) tick.datetime = generate_datetime(float(data["E"])) else: kline_data: dict = data["k"] bar_ready: bool = kline_data.get("x", False) if not bar_ready: return dt: datetime = generate_datetime(float(kline_data["t"])) if tick.extra is None: tick.extra = {} tick.extra["bar"] = BarData( symbol=contract.symbol, exchange=Exchange.GLOBAL, datetime=dt.replace(second=0, microsecond=0), interval=Interval.MINUTE, volume=float(kline_data["v"]), turnover=float(kline_data["q"]), open_price=float(kline_data["o"]), high_price=float(kline_data["h"]), low_price=float(kline_data["l"]), close_price=float(kline_data["c"]), gateway_name=self.gateway_name ) if tick.last_price: tick.localtime = datetime.now() self.gateway.on_tick(copy(tick)) def on_disconnected(self, status_code: int, msg: str) -> None: """Callback when server is disconnected.""" self.gateway.write_log(f"CM MD API disconnected, code: {status_code}, msg: {msg}") def on_error(self, e: Exception) -> None: """Callback when exception raised.""" self.gateway.write_log(f"CM MD API exception: {e}") class MarginMdApi(WebsocketClient): """ The margin/spot market data websocket API. Handles real-time updates for: - Tickers (24hr statistics) - Book ticker (best bid/ask) - Klines (candlestick data) if enabled """ def __init__(self, gateway: BinancePortfolioGateway) -> None: """Initialize the API.""" super().__init__() self.gateway: BinancePortfolioGateway = gateway self.gateway_name: str = gateway.gateway_name self.ticks: dict[str, TickData] = {} self.reqid: int = 0 self.kline_stream: bool = False def connect( self, server: str, kline_stream: bool, proxy_host: str, proxy_port: int, ) -> None: """Start server connection.""" self.kline_stream = kline_stream if server == "REAL": self.init(REAL_MARGIN_DATA_HOST, proxy_host, proxy_port, receive_timeout=WEBSOCKET_TIMEOUT) else: self.init(TESTNET_MARGIN_DATA_HOST, proxy_host, proxy_port, receive_timeout=WEBSOCKET_TIMEOUT) self.start() def on_connected(self) -> None: """Callback when server is connected.""" self.gateway.write_log("Margin MD API connected") if self.ticks: channels = [] for symbol in self.ticks.keys(): channels.append(f"{symbol}@ticker") channels.append(f"{symbol}@bookTicker") if self.kline_stream: channels.append(f"{symbol}@kline_1m") packet: dict = { "method": "SUBSCRIBE", "params": channels, "id": self.reqid } self.send_packet(packet) def subscribe(self, req: SubscribeRequest) -> None: """Subscribe to market data.""" if req.symbol in self.ticks: return contract: ContractData | None = self.gateway.get_contract_by_symbol(req.symbol) if not contract: self.gateway.write_log(f"Failed to subscribe data, symbol not found: {req.symbol}") return self.reqid += 1 tick: TickData = TickData( symbol=req.symbol, name=contract.name, exchange=Exchange.GLOBAL, datetime=datetime.now(UTC_TZ), gateway_name=self.gateway_name, ) tick.extra = {} self.ticks[req.symbol] = tick channels: list[str] = [ f"{contract.name.lower()}@ticker", f"{contract.name.lower()}@bookTicker" ] if self.kline_stream: channels.append(f"{contract.name.lower()}@kline_1m") packet: dict = { "method": "SUBSCRIBE", "params": channels, "id": self.reqid } self.send_packet(packet) def on_packet(self, packet: dict) -> None: """Callback of market data update.""" # Handle stream format stream: str = packet.get("stream", "") if stream: data: dict = packet["data"] name, channel = stream.split("@") contract: ContractData | None = self.gateway.get_contract_by_name(name.upper(), MarketType.MARGIN) if not contract: return tick: TickData | None = self.ticks.get(contract.symbol) if not tick: return if channel == "ticker": tick.volume = float(data["v"]) tick.turnover = float(data["q"]) tick.open_price = float(data["o"]) tick.high_price = float(data["h"]) tick.low_price = float(data["l"]) tick.last_price = float(data["c"]) tick.datetime = generate_datetime(float(data["E"])) elif channel == "bookTicker": tick.bid_price_1 = float(data["b"]) tick.bid_volume_1 = float(data["B"]) tick.ask_price_1 = float(data["a"]) tick.ask_volume_1 = float(data["A"]) tick.datetime = generate_datetime(float(data["E"])) elif channel.startswith("kline"): kline_data: dict = data["k"] bar_ready: bool = kline_data.get("x", False) if not bar_ready: return dt: datetime = generate_datetime(float(kline_data["t"])) if tick.extra is None: tick.extra = {} tick.extra["bar"] = BarData( symbol=contract.symbol, exchange=Exchange.GLOBAL, datetime=dt.replace(second=0, microsecond=0), interval=Interval.MINUTE, volume=float(kline_data["v"]), turnover=float(kline_data["q"]), open_price=float(kline_data["o"]), high_price=float(kline_data["h"]), low_price=float(kline_data["l"]), close_price=float(kline_data["c"]), gateway_name=self.gateway_name ) if tick.last_price: tick.localtime = datetime.now() self.gateway.on_tick(copy(tick)) return # Handle non-stream format (bookTicker) symbol_name: str = packet.get("s", "") if not symbol_name: return contract = self.gateway.get_contract_by_name(symbol_name, MarketType.MARGIN) if not contract: return tick = self.ticks.get(contract.symbol) if not tick: return channel = packet.get("e", "bookTicker") if channel == "24hrTicker": tick.volume = float(packet["v"]) tick.turnover = float(packet["q"]) tick.open_price = float(packet["o"]) tick.high_price = float(packet["h"]) tick.low_price = float(packet["l"]) tick.last_price = float(packet["c"]) tick.datetime = generate_datetime(float(packet["E"])) elif channel == "bookTicker": tick.bid_price_1 = float(packet["b"]) tick.bid_volume_1 = float(packet["B"]) tick.ask_price_1 = float(packet["a"]) tick.ask_volume_1 = float(packet["A"]) if tick.last_price: tick.localtime = datetime.now() self.gateway.on_tick(copy(tick)) def on_disconnected(self, status_code: int, msg: str) -> None: """Callback when server is disconnected.""" self.gateway.write_log(f"Margin MD API disconnected, code: {status_code}, msg: {msg}") def on_error(self, e: Exception) -> None: """Callback when exception raised.""" self.gateway.write_log(f"Margin MD API exception: {e}") ================================================ FILE: vnpy_binance/spot_gateway.py ================================================ import hashlib import hmac import time import urllib.parse from copy import copy from collections.abc import Callable from time import sleep from datetime import datetime, timedelta from numpy import format_float_positional from vnpy.event import EventEngine, Event from vnpy.trader.event import EVENT_TIMER from vnpy.trader.constant import ( Direction, Exchange, Product, Status, OrderType, Interval ) from vnpy.trader.gateway import BaseGateway from vnpy.trader.object import ( TickData, OrderData, TradeData, AccountData, ContractData, BarData, OrderRequest, CancelRequest, SubscribeRequest, HistoryRequest ) from vnpy.trader.utility import ZoneInfo from vnpy_rest import Request, RestClient, Response from vnpy_websocket import WebsocketClient # Timezone constant UTC_TZ = ZoneInfo("UTC") # Real server hosts REAL_REST_HOST: str = "https://api.binance.com" REAL_TRADE_HOST: str = "wss://ws-api.binance.com/ws-api/v3" REAL_DATA_HOST: str = "wss://stream.binance.com:443" # Testnet server hosts TESTNET_REST_HOST: str = "https://testnet.binance.vision" TESTNET_TRADE_HOST: str = "wss://ws-api.testnet.binance.vision/ws-api/v3" TESTNET_DATA_HOST: str = "wss://stream.testnet.binance.vision/ws" # Order status map STATUS_BINANCE2VT: dict[str, Status] = { "NEW": Status.NOTTRADED, "PARTIALLY_FILLED": Status.PARTTRADED, "FILLED": Status.ALLTRADED, "CANCELED": Status.CANCELLED, "REJECTED": Status.REJECTED, "EXPIRED": Status.CANCELLED } # Order type map ORDERTYPE_VT2BINANCE: dict[OrderType, tuple[str, str]] = { OrderType.LIMIT: ("LIMIT", "GTC"), OrderType.MARKET: ("MARKET", "GTC"), OrderType.FAK: ("LIMIT", "IOC"), OrderType.FOK: ("LIMIT", "FOK"), } ORDERTYPE_BINANCE2VT: dict[tuple[str, str], OrderType] = {v: k for k, v in ORDERTYPE_VT2BINANCE.items()} # Direction map DIRECTION_VT2BINANCE: dict[Direction, str] = { Direction.LONG: "BUY", Direction.SHORT: "SELL" } DIRECTION_BINANCE2VT: dict[str, Direction] = {v: k for k, v in DIRECTION_VT2BINANCE.items()} # Kline interval map INTERVAL_VT2BINANCE: dict[Interval, str] = { Interval.MINUTE: "1m", Interval.HOUR: "1h", Interval.DAILY: "1d", } # Timedelta map TIMEDELTA_MAP: dict[Interval, timedelta] = { Interval.MINUTE: timedelta(minutes=1), Interval.HOUR: timedelta(hours=1), Interval.DAILY: timedelta(days=1), } # Set weboscket timeout to 24 hour WEBSOCKET_TIMEOUT = 24 * 60 * 60 class BinanceSpotGateway(BaseGateway): """ The Binance spot trading gateway for VeighNa. This gateway provides trading functionality for Binance spot markets through their API. Features: 1. Provides market data, trading, and account management capabilities 2. Supports limit, market, FOK, and FAK order types 3. Handles real-time account balance updates """ default_name: str = "BINANCE_SPOT" default_setting: dict = { "API Key": "", "API Secret": "", "Server": ["REAL", "TESTNET"], "Kline Stream": ["False", "True"], "Proxy Host": "", "Proxy Port": 0 } exchanges: list[Exchange] = [Exchange.GLOBAL] def __init__(self, event_engine: EventEngine, gateway_name: str) -> None: """ The init method of the gateway. This method initializes the gateway components including REST API, trading API, user data API, and market data API. It also sets up the data structures for order and contract storage. Parameters: event_engine: the global event engine object of VeighNa gateway_name: the unique name for identifying the gateway """ super().__init__(event_engine, gateway_name) self.trade_api: TradeApi = TradeApi(self) self.md_api: MdApi = MdApi(self) self.rest_api: RestApi = RestApi(self) self.orders: dict[str, OrderData] = {} self.symbol_contract_map: dict[str, ContractData] = {} self.name_contract_map: dict[str, ContractData] = {} def connect(self, setting: dict) -> None: """ Start server connections. This method establishes connections to Binance servers using the provided settings. Parameters: setting: A dictionary containing connection parameters including API credentials, server selection, and proxy configuration """ key: str = setting["API Key"] secret: str = setting["API Secret"] server: str = setting["Server"] kline_stream: bool = setting["Kline Stream"] == "True" proxy_host: str = setting["Proxy Host"] proxy_port: int = setting["Proxy Port"] self.rest_api.connect(key, secret, server, proxy_host, proxy_port) self.trade_api.connect(key, secret, server, proxy_host, proxy_port) self.md_api.connect(server, kline_stream, proxy_host, proxy_port) self.event_engine.register(EVENT_TIMER, self.process_timer_event) def subscribe(self, req: SubscribeRequest) -> None: """ Subscribe to market data. This method forwards the subscription request to the market data API. Parameters: req: Subscription request object containing the symbol to subscribe """ self.md_api.subscribe(req) def send_order(self, req: OrderRequest) -> str: """ Send new order. This method forwards the order request to the trading API. Parameters: req: Order request object containing order details Returns: str: The VeighNa order ID if successful, empty string if failed """ return self.trade_api.send_order(req) def cancel_order(self, req: CancelRequest) -> None: """ Cancel existing order. This method forwards the cancellation request to the trading API. Parameters: req: Cancel request object containing order details """ self.trade_api.cancel_order(req) def query_account(self) -> None: """ Query account balance. Not required since Binance provides websocket updates for account balances. """ pass def query_position(self) -> None: """ Query current positions. Not implemented for spot trading as there is no position concept. """ pass def query_history(self, req: HistoryRequest) -> list[BarData]: """ Query historical kline data. This method forwards the history request to the REST API. Parameters: req: History request object containing query parameters Returns: list[BarData]: List of historical kline data bars """ return self.rest_api.query_history(req) def close(self) -> None: """ Close server connections. This method stops all API connections and releases resources. """ self.rest_api.stop() self.md_api.stop() self.trade_api.stop() def on_order(self, order: OrderData) -> None: """ Save a copy of order and then push to event engine. Parameters: order: Order data object """ self.orders[order.orderid] = copy(order) super().on_order(order) def get_order(self, orderid: str) -> OrderData | None: """ Get previously saved order by order id. Parameters: orderid: The ID of the order to retrieve Returns: Order data object if found, None otherwise """ return self.orders.get(orderid, None) def on_contract(self, contract: ContractData) -> None: """ Save contract data in mappings and push to event engine. Parameters: contract: Contract data object """ self.symbol_contract_map[contract.symbol] = contract self.name_contract_map[contract.name] = contract super().on_contract(contract) def get_contract_by_symbol(self, symbol: str) -> ContractData | None: """ Get contract data by VeighNa symbol. Parameters: symbol: VeighNa symbol (e.g. "BTC_USDT_BINANCE") Returns: Contract data object if found, None otherwise """ return self.symbol_contract_map.get(symbol, None) def get_contract_by_name(self, name: str) -> ContractData | None: """ Get contract data by exchange symbol name. Parameters: name: Exchange symbol name (e.g. "BTCUSDT") Returns: Contract data object if found, None otherwise """ return self.name_contract_map.get(name, None) def process_timer_event(self, event: Event) -> None: """ Process timer task. This function is called regularly by the event engine to perform scheduled tasks, such as keeping the user stream alive. Parameters: event: Timer event object """ self.md_api.subscribe_new_channels() class RestApi(RestClient): """ The REST API of BinanceSpotGateway. This class handles HTTP requests to Binance API endpoints, including: - Authentication and signature generation - Contract information queries - Account balance queries - Order management - Historical data queries - User data stream management """ def __init__(self, gateway: BinanceSpotGateway) -> None: """ The init method of the API. This method initializes the REST API with a reference to the parent gateway. Parameters: gateway: the parent gateway object for pushing callback data """ super().__init__() self.gateway: BinanceSpotGateway = gateway self.gateway_name: str = gateway.gateway_name self.key: str = "" self.secret: bytes = b"" self.time_offset: int = 0 self.order_count: int = 1_000_000 self.order_prefix: str = "" def sign(self, request: Request) -> Request: """ Standard callback for signing a request. This method adds the necessary authentication parameters and signature to requests that require API key authentication. It handles: 1. Path construction with query parameters 2. Timestamp generation with server time offset adjustment 3. HMAC-SHA256 signature generation 4. Required authentication headers Parameters: request: Request object to be signed Returns: Request: Modified request with authentication parameters """ if request.data and request.data.get("signed", False): # Construct path with query parameters if they exist if request.params: path: str = request.path + "?" + urllib.parse.urlencode(request.params) else: request.params = {} path = request.path # Get current timestamp in milliseconds timestamp: int = int(time.time() * 1000) # Adjust timestamp based on time offset with server if self.time_offset > 0: timestamp -= abs(self.time_offset) elif self.time_offset < 0: timestamp += abs(self.time_offset) # Add timestamp to request parameters request.params["timestamp"] = timestamp # Generate signature using HMAC SHA256 query: str = urllib.parse.urlencode(sorted(request.params.items())) signature: str = hmac.new( self.secret, query.encode("utf-8"), hashlib.sha256 ).hexdigest() # Append signature to query string query += f"&signature={signature}" path = request.path + "?" + query # Update request with signed path and clear params/data request.path = path request.params = {} request.data = {} # Add required headers for API authentication request.headers = { "Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json", "X-MBX-APIKEY": self.key, "Connection": "close" } return request def connect( self, key: str, secret: str, server: str, proxy_host: str, proxy_port: int ) -> None: """Start server connection""" self.key = key self.secret = secret.encode() self.proxy_port = proxy_port self.proxy_host = proxy_host self.server = server self.order_prefix = datetime.now().strftime("%y%m%d%H%M%S") if self.server == "REAL": self.init(REAL_REST_HOST, proxy_host, proxy_port) else: self.init(TESTNET_REST_HOST, proxy_host, proxy_port) self.start() self.gateway.write_log("REST API started") self.query_time() def query_time(self) -> None: """ Query server time to calculate local time offset. This function sends a request to get the exchange server time, which is used to calculate the local time offset for timestamp synchronization. """ path: str = "/api/v3/time" self.add_request( "GET", path, callback=self.on_query_time ) def query_account(self) -> None: """ Query account balance. This function sends a request to get the account balance information. """ path: str = "/api/v3/account" self.add_request( method="GET", path=path, callback=self.on_query_account, data={"signed": True} ) def query_order(self) -> None: """ Query open orders. This function sends a request to get all active orders that have not been fully filled or cancelled. """ path: str = "/api/v3/openOrders" self.add_request( method="GET", path=path, callback=self.on_query_order, data={"signed": True} ) def query_contract(self) -> None: """ Query available contracts. This function sends a request to get exchange information, including all available trading instruments, their precision, and trading rules. """ path: str = "/api/v3/exchangeInfo" self.add_request( method="GET", path=path, callback=self.on_query_contract, ) def on_query_time(self, data: dict, request: Request) -> None: """ Callback of server time query. This function processes the server time response and calculates the time offset between local and server time, which is used for request timestamp synchronization. Parameters: data: Response data from the server request: Original request object """ local_time: int = int(time.time() * 1000) server_time: int = int(data["serverTime"]) self.time_offset = local_time - server_time self.gateway.write_log(f"Server time updated, local offset: {self.time_offset}ms") self.query_contract() def on_query_account(self, data: dict, request: Request) -> None: """ Callback of account balance query. This function processes the account balance response and creates AccountData objects for each asset in the account. Parameters: data: Response data from the server request: Original request object """ for balance in data["balances"]: asset = balance["asset"] free = float(balance["free"]) locked = float(balance["locked"]) if free or locked: account: AccountData = AccountData( accountid=asset, balance=free + locked, frozen=locked, gateway_name=self.gateway_name ) self.gateway.on_account(account) self.gateway.write_log("Account data received") def on_query_order(self, data: list, request: Request) -> None: """ Callback of open orders query. This function processes the open orders response and creates OrderData objects for each active order. Parameters: data: Response data from the server request: Original request object """ for d in data: key: tuple[str, str] = (d["type"], d["timeInForce"]) order_type: OrderType | None = ORDERTYPE_BINANCE2VT.get(key, None) if not order_type: continue contract: ContractData | None = self.gateway.get_contract_by_name(d["symbol"]) if not contract: continue order: OrderData = OrderData( orderid=d["clientOrderId"], symbol=contract.symbol, exchange=Exchange.GLOBAL, price=float(d["price"]), volume=float(d["origQty"]), type=order_type, direction=DIRECTION_BINANCE2VT[d["side"]], traded=float(d["executedQty"]), status=STATUS_BINANCE2VT[d["status"]], datetime=generate_datetime(d["time"]), gateway_name=self.gateway_name, ) self.gateway.on_order(order) self.gateway.write_log("Order data received") def on_query_contract(self, data: dict, request: Request) -> None: """ Callback of available contracts query. This function processes the exchange info response and creates ContractData objects for each trading instrument. It extracts trading rules like price tick, minimum/maximum volumes from filters. Parameters: data: Response data from the server request: Original request object """ for d in data["symbols"]: pricetick: float = 1 min_volume: float = 1 max_volume: float = 1 for f in d["filters"]: if f["filterType"] == "PRICE_FILTER": pricetick = float(f["tickSize"]) elif f["filterType"] == "LOT_SIZE": min_volume = float(f["minQty"]) max_volume = float(f["maxQty"]) symbol: str = d["symbol"] + "_SPOT_BINANCE" contract: ContractData = ContractData( symbol=symbol, exchange=Exchange.GLOBAL, name=d["symbol"], pricetick=pricetick, size=1, min_volume=min_volume, max_volume=max_volume, product=Product.SPOT, net_position=True, history_data=True, gateway_name=self.gateway_name, stop_supported=False ) self.gateway.on_contract(contract) self.gateway.write_log("Contract data received") # Query private data after time offset is calculated if self.key and self.secret: self.query_order() self.query_account() self.gateway.trade_api.subscribe_user_data_stream() def query_history(self, req: HistoryRequest) -> list[BarData]: """Query kline history data""" # Check if the contract and interval exist contract: ContractData | None = self.gateway.get_contract_by_symbol(req.symbol) if not contract: return [] if not req.interval: return [] # Prepare history list history: list[BarData] = [] limit: int = 1000 # Convert start time to milliseconds start_time: int = int(datetime.timestamp(req.start)) while True: # Create query parameters params: dict = { "symbol": contract.name, "interval": INTERVAL_VT2BINANCE[req.interval], "limit": limit } params["startTime"] = start_time * 1000 path: str = "/api/v3/klines" if req.end: end_time = int(datetime.timestamp(req.end)) params["endTime"] = end_time * 1000 # Convert to milliseconds resp: Response = self.request( "GET", path=path, params=params ) # Break the loop if request failed if resp.status_code // 100 != 2: msg: str = f"Query kline history failed, status code: {resp.status_code}, message: {resp.text}" self.gateway.write_log(msg) break else: data: dict = resp.json() if not data: msg = f"No kline history data is received, start time: {start_time}" self.gateway.write_log(msg) break buf: list[BarData] = [] for row in data: bar: BarData = BarData( symbol=req.symbol, exchange=req.exchange, datetime=generate_datetime(row[0]), interval=req.interval, volume=float(row[5]), turnover=float(row[7]), open_price=float(row[1]), high_price=float(row[2]), low_price=float(row[3]), close_price=float(row[4]), gateway_name=self.gateway_name ) buf.append(bar) begin: datetime = buf[0].datetime end: datetime = buf[-1].datetime history.extend(buf) msg = f"Query kline history finished, {req.symbol} - {req.interval.value}, {begin} - {end}" self.gateway.write_log(msg) next_start_dt = bar.datetime + TIMEDELTA_MAP[req.interval] next_start_time = int(datetime.timestamp(next_start_dt)) # Break the loop if the latest data received if ( len(data) < limit or (req.end and next_start_dt >= req.end) ): break # Update query start time start_time = next_start_time # Wait to meet request flow limit sleep(0.5) # Remove the unclosed kline if history: history.pop(-1) return history class MdApi(WebsocketClient): """ The market data websocket API of BinanceSpotGateway. This class handles market data from Binance through websocket connection. It processes real-time updates for: - Tickers (24hr statistics) - Order book depth (10 levels) - Klines (candlestick data) if enabled """ def __init__(self, gateway: BinanceSpotGateway) -> None: """ The init method of the API. This method initializes the websocket client with a reference to the parent gateway. Parameters: gateway: the parent gateway object for pushing callback data """ super().__init__() self.gateway: BinanceSpotGateway = gateway self.gateway_name: str = gateway.gateway_name self.ticks: dict[str, TickData] = {} self.reqid: int = 0 self.kline_stream: bool = False self.new_channels: list[str] = [] def connect( self, server: str, kline_stream: bool, proxy_host: str, proxy_port: int, ) -> None: """ Start server connection. This method establishes a websocket connection to Binance market data stream. Parameters: server: Server type ("REAL" or "TESTNET") kline_stream: Whether to include kline data stream proxy_host: Proxy server hostname or IP proxy_port: Proxy server port """ self.kline_stream = kline_stream if server == "REAL": self.init(REAL_DATA_HOST, proxy_host, proxy_port, receive_timeout=WEBSOCKET_TIMEOUT) else: self.init(TESTNET_DATA_HOST, proxy_host, proxy_port, receive_timeout=WEBSOCKET_TIMEOUT) self.start() def on_connected(self) -> None: """ Callback when server is connected. This function is called when the market data websocket connection is successfully established. It logs the connection status and resubscribes to previously subscribed market data channels. """ self.gateway.write_log("MD API connected") # Resubscribe market data if self.ticks: channels = [] for symbol in self.ticks.keys(): channels.append(f"{symbol}@ticker") channels.append(f"{symbol}@bookTicker") if self.kline_stream: channels.append(f"{symbol}@kline_1m") packet: dict = { "method": "SUBSCRIBE", "params": channels, "id": self.reqid } self.send_packet(packet) def subscribe(self, req: SubscribeRequest) -> None: """ Subscribe to market data. This function sends subscription requests for ticker and depth data for the specified trading instrument. If kline_stream is enabled, it will also subscribe to 1-minute kline data. Parameters: req: Subscription request object containing symbol information """ if req.symbol in self.ticks: return contract: ContractData | None = self.gateway.get_contract_by_symbol(req.symbol) if not contract: self.gateway.write_log(f"Failed to subscribe data, symbol not found: {req.symbol}") return self.reqid += 1 # Initialize tick object tick: TickData = TickData( symbol=req.symbol, name=contract.name, exchange=Exchange.GLOBAL, datetime=datetime.now(UTC_TZ), gateway_name=self.gateway_name, ) tick.extra = {} self.ticks[req.symbol] = tick channels: list[str] = [ f"{contract.name.lower()}@ticker", f"{contract.name.lower()}@bookTicker" ] if self.kline_stream: channels.append(f"{contract.name.lower()}@kline_1m") self.new_channels.extend(channels) def subscribe_new_channels(self) -> None: """ Update timer event. This function sends subscription requests for new channels to the market data websocket server. """ if not self.new_channels: return packet: dict = { "method": "SUBSCRIBE", "params": self.new_channels, "id": self.reqid } self.send_packet(packet) self.new_channels = [] def on_packet(self, packet: dict) -> None: """ Callback of market data update. This function processes different types of market data updates, including ticker, depth, and kline data. It updates the corresponding TickData object and pushes updates to the gateway. Parameters: packet: JSON data received from websocket """ name: str = packet.get("s", "") if not name: return contract: ContractData | None = self.gateway.get_contract_by_name(name.upper()) if not contract: return tick: TickData = self.ticks[contract.symbol] channel: str = packet.get("e", "bookTicker") if channel == "24hrTicker": tick.volume = float(packet["v"]) tick.turnover = float(packet["q"]) tick.open_price = float(packet["o"]) tick.high_price = float(packet["h"]) tick.low_price = float(packet["l"]) tick.last_price = float(packet["c"]) tick.datetime = generate_datetime(float(packet["E"])) elif channel == "bookTicker": tick.bid_price_1 = float(packet["b"]) tick.bid_volume_1 = float(packet["B"]) tick.ask_price_1 = float(packet["a"]) tick.ask_volume_1 = float(packet["A"]) elif channel == "kline": # Handle kline data kline_data: dict = packet["k"] # Check if bar is closed bar_ready: bool = kline_data.get("x", False) if not bar_ready: return if tick.extra is None: tick.extra = {} dt: datetime = generate_datetime(float(kline_data["t"])) tick.extra["bar"] = BarData( symbol=name.upper(), exchange=Exchange.GLOBAL, datetime=dt.replace(second=0, microsecond=0), interval=Interval.MINUTE, volume=float(kline_data["v"]), turnover=float(kline_data["q"]), open_price=float(kline_data["o"]), high_price=float(kline_data["h"]), low_price=float(kline_data["l"]), close_price=float(kline_data["c"]), gateway_name=self.gateway_name ) # According to Binance API updates, /api/v3/myTrades now returns quoteQty if "Q" in kline_data: tick.extra["bar"].turnover = float(kline_data["Q"]) if tick.last_price: tick.localtime = datetime.now() self.gateway.on_tick(copy(tick)) def on_disconnected(self, status_code: int, msg: str) -> None: """ Callback when server is disconnected. This function is called when the market data websocket connection is closed. It logs the disconnection details. Parameters: status_code: HTTP status code for the disconnection msg: Disconnection message """ self.gateway.write_log(f"MD API disconnected, code: {status_code}, msg: {msg}") def on_error(self, e: Exception) -> None: """ Callback when exception raised. This function is called when an exception occurs in the market data websocket connection. It logs the exception details for troubleshooting. Parameters: e: The exception that was raised """ self.gateway.write_log(f"MD API exception: {e}") class TradeApi(WebsocketClient): """ The trading websocket API of BinanceSpotGateway. This class handles trading operations with Binance through websocket connection. It provides functionality for: - Order placement - Order cancellation - Request authentication and signature generation Note: According to Binance API updates from 2025-04-25, some request weights have been increased. For example, order amend operations now have a weight of 4 instead of 1. """ def __init__(self, gateway: BinanceSpotGateway) -> None: """ The init method of the API. This method initializes the websocket client with a reference to the parent gateway. Parameters: gateway: the parent gateway object for pushing callback data """ super().__init__() self.gateway: BinanceSpotGateway = gateway self.gateway_name: str = gateway.gateway_name self.key: str = "" self.secret: bytes = b"" self.proxy_port: int = 0 self.proxy_host: str = "" self.server: str = "" self.reqid: int = 0 self.order_count: int = 0 self.order_prefix: str = "" self.reqid_callback_map: dict[int, Callable] = {} self.reqid_order_map: dict[int, OrderData] = {} self.user_stream_subscribed: bool = False def connect( self, key: str, secret: str, server: str, proxy_host: str, proxy_port: int ) -> None: """ Start server connection. This method initializes the API credentials and establishes a websocket connection to Binance trading API. Parameters: key: API Key for authentication secret: API Secret for request signing server: Server type ("REAL" or "TESTNET") proxy_host: Proxy server hostname or IP proxy_port: Proxy server port """ self.key = key self.secret = secret.encode() self.proxy_port = proxy_port self.proxy_host = proxy_host self.server = server self.order_prefix = datetime.now().strftime("%y%m%d%H%M%S") if self.server == "REAL": self.init(REAL_TRADE_HOST, proxy_host, proxy_port) else: self.init(TESTNET_TRADE_HOST, proxy_host, proxy_port) self.start() def sign(self, params: dict) -> None: """ Generate the signature for the request. This function creates an HMAC-SHA256 signature required for authenticated API requests to Binance. Parameters: params: Dictionary containing the parameters to be signed """ timestamp: int = int(time.time() * 1000) params["timestamp"] = timestamp payload: str = "&".join([f"{k}={v}" for k, v in sorted(params.items())]) signature: str = hmac.new( self.secret, payload.encode("utf-8"), hashlib.sha256 ).hexdigest() params["signature"] = signature def send_order(self, req: OrderRequest) -> str: """ Send new order to Binance. This function creates and sends a new order request to the exchange. It handles different order types including market and limit orders. Parameters: req: Order request object containing order details Returns: vt_orderid: The VeighNa order ID (gateway_name.orderid) if successful, empty string otherwise """ # Get contract contract: ContractData | None = self.gateway.get_contract_by_symbol(req.symbol) if not contract: self.gateway.write_log(f"Failed to send order, symbol not found: {req.symbol}") return "" # Generate new order id self.order_count += 1 orderid: str = self.order_prefix + str(self.order_count) # Push a submitting order event order: OrderData = req.create_order_data( orderid, self.gateway_name ) self.gateway.on_order(order) # Create order parameters params: dict = { "apiKey": self.key, "symbol": contract.name, "side": DIRECTION_VT2BINANCE[req.direction], "quantity": format_float(req.volume), "newClientOrderId": orderid, } if req.type == OrderType.MARKET: params["type"] = "MARKET" else: order_type, time_condition = ORDERTYPE_VT2BINANCE[req.type] params["type"] = order_type params["timeInForce"] = time_condition params["price"] = format_float(req.price) self.sign(params) self.reqid += 1 self.reqid_callback_map[self.reqid] = self.on_send_order self.reqid_order_map[self.reqid] = order packet: dict = { "id": self.reqid, "method": "order.place", "params": params, } self.send_packet(packet) return order.vt_orderid def cancel_order(self, req: CancelRequest) -> None: """ Cancel existing order on Binance. This function sends a request to cancel an existing order on the exchange. Parameters: req: Cancel request object containing order details """ contract: ContractData | None = self.gateway.get_contract_by_symbol(req.symbol) if not contract: self.gateway.write_log(f"Failed to cancel order, symbol not found: {req.symbol}") return params: dict = { "apiKey": self.key, "symbol": contract.name, "origClientOrderId": req.orderid } self.sign(params) self.reqid += 1 self.reqid_callback_map[self.reqid] = self.on_cancel_order packet: dict = { "id": self.reqid, "method": "order.cancel", "params": params, } self.send_packet(packet) def subscribe_user_data_stream(self) -> None: """ Subscribe to user data stream. This function sends a subscription request to receive real-time account and order updates through the websocket API connection. """ if not self.key or self.user_stream_subscribed: return self.user_stream_subscribed = True params: dict = {"apiKey": self.key} self.sign(params) self.reqid += 1 self.reqid_callback_map[self.reqid] = self.on_subscribe_user_data_stream packet: dict = { "id": self.reqid, "method": "userDataStream.subscribe.signature", "params": params, } self.send_packet(packet) def on_subscribe_user_data_stream(self, packet: dict) -> None: """ Callback of user data stream subscription. This function processes the response to the user data stream subscription request. It logs errors if the subscription fails. Parameters: packet: JSON data received from websocket """ error: dict | None = packet.get("error", None) if error: self.user_stream_subscribed = False error_code: str = error["code"] error_msg: str = error["msg"] self.gateway.write_log( f"User data stream subscription failed, code: {error_code}, message: {error_msg}" ) return self.gateway.write_log("User data stream subscribed") def on_connected(self) -> None: """ Callback when server is connected. This function is called when the trading websocket connection is successfully established. It logs the connection status and subscribes to the user data stream. """ self.gateway.write_log("Trade API connected") self.subscribe_user_data_stream() def on_disconnected(self, status_code: int, msg: str) -> None: """ Callback when server is disconnected. This function is called when the trading websocket connection is closed. It logs the disconnection details. Parameters: status_code: HTTP status code for the disconnection msg: Disconnection message """ self.gateway.write_log(f"Trade API disconnected, code: {status_code}, msg: {msg}") self.user_stream_subscribed = False def on_packet(self, packet: dict) -> None: """ Callback of data update. This function processes responses from the trading websocket API and user data stream events. Trade responses are routed by request ID, while user data events are routed by event type. Parameters: packet: JSON data received from websocket """ # User data stream event event: dict | None = packet.get("event", None) if event: self.on_user_data_event(event) return # Trade API response reqid: int = packet.get("id", 0) callback: Callable | None = self.reqid_callback_map.get(reqid, None) if callback: callback(packet) def on_user_data_event(self, event: dict) -> None: """ Process user data stream event. This function routes user data stream events to the appropriate handler based on the event type. Parameters: event: Event payload from user data stream """ match event["e"]: case "outboundAccountPosition": self.on_account(event) case "executionReport": self.on_order(event) def on_account(self, event: dict) -> None: """ Callback of account balance update. This function processes the account update event from the user data stream, including balance changes. Parameters: event: Event payload from user data stream """ for balance in event["B"]: asset = balance["a"] free = float(balance["f"]) locked = float(balance["l"]) if free or locked: account: AccountData = AccountData( accountid=asset, balance=free + locked, frozen=locked, gateway_name=self.gateway_name ) self.gateway.on_account(account) def on_order(self, event: dict) -> None: """ Callback of order and trade update. This function processes the order update event from the user data stream, including order status changes and trade executions. Parameters: event: Event payload from user data stream """ symbol: str = event["s"] if not event["C"]: orderid: str = event["c"] else: orderid = event["C"] type_str: str = event["o"] time_in_force: str = event["f"] key: tuple[str, str] = (type_str, time_in_force) order_type: OrderType | None = ORDERTYPE_BINANCE2VT.get(key, None) if not order_type: return contract = self.gateway.get_contract_by_name(symbol) if not contract: return order = OrderData( symbol=contract.symbol, exchange=Exchange.GLOBAL, orderid=orderid, type=order_type, direction=DIRECTION_BINANCE2VT[event["S"]], price=float(event["p"]), volume=float(event["q"]), traded=float(event["z"]), status=STATUS_BINANCE2VT[event["X"]], datetime=generate_datetime(event["E"]), gateway_name=self.gateway_name, ) self.gateway.on_order(order) if float(event["l"]) <= 0: return trade_volume = float(event["l"]) trade = TradeData( symbol=contract.symbol, exchange=Exchange.GLOBAL, orderid=orderid, tradeid=event["t"], direction=DIRECTION_BINANCE2VT[event["S"]], price=float(event["L"]), volume=trade_volume, datetime=generate_datetime(event["T"]), gateway_name=self.gateway_name, ) self.gateway.on_trade(trade) def on_send_order(self, packet: dict) -> None: """ Callback of send order. This function processes the response to an order placement request. It handles errors by logging the details and updating the order status. Parameters: packet: JSON data received from websocket """ error: dict | None = packet.get("error", None) if not error: return error_code: str = error["code"] error_msg: str = error["msg"] msg: str = f"Order rejected, code: {error_code}, message: {error_msg}" self.gateway.write_log(msg) reqid: int = packet.get("id", 0) order: OrderData | None = self.reqid_order_map.get(reqid, None) if order: order.status = Status.REJECTED self.gateway.on_order(order) def on_cancel_order(self, packet: dict) -> None: """ Callback of cancel order. This function processes the response to an order cancellation request. It handles errors by logging the details. Parameters: packet: JSON data received from websocket """ error: dict | None = packet.get("error", None) if not error: return error_code: str = error["code"] error_msg: str = error["msg"] msg: str = f"Cancel rejected, code: {error_code}, message: {error_msg}" self.gateway.write_log(msg) def on_error(self, e: Exception) -> None: """ Callback when exception raised. This function is called when an exception occurs in the trading websocket connection. It logs the exception details for troubleshooting. Parameters: e: The exception that was raised """ self.gateway.write_log(f"Trade API exception: {e}") def generate_datetime(timestamp: float) -> datetime: """ Generate datetime object from Binance timestamp. This function converts a Binance millisecond timestamp to a datetime object with UTC timezone. Parameters: timestamp: Binance timestamp in milliseconds Returns: Datetime object with UTC timezone """ dt: datetime = datetime.fromtimestamp(timestamp / 1000, tz=UTC_TZ) return dt def format_float(f: float) -> str: """ Convert float number to string with correct precision. This function formats floating point numbers to avoid precision errors when sending requests to Binance. Parameters: f: The floating point number to format Returns: Formatted string representation of the number Note: Fixes potential error -1111: Parameter "quantity" has too much precision """ return format_float_positional(f, trim="-")