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="-")