pip install httptrading
市面上有很多交易通道或者加密货币交易所提供了行情、交易的接口, 不同的接口提供方提供各自的接入方式, 如果你同时使用多家的接口服务, 必然需要兼容诸如标的代码格式, 报价单位类型, 下单参数, 判断订单生命周期, 接口限速.
另外的, 混合接入多家 SDK 在一个项目, 会遇到一些兼容性问题: 包里面的依赖互相版本冲突, 或者部署的平台、二进制模块发布导致有限 python 的版本支持.
所以, 这个项目打算使用一套统一格式的 http 接口, 把各种接口提供方的调用隐藏, 缺点是每家的功能均会有限制. 支持一些主要的交易动作, 比如查特定持仓, 可用资金, 下限价单以及市价单, 也可以基于基类, 自行编写自定义的其他交易平台接口.
针对兼容性问题, 则可以把不兼容的 SDK 放在不同进程中, 用特定的启动脚本运行, 如果做得好一点, 再用反向代理软件封装端口的流量.
这个项目默认支持如下交易平台:
交易通道 | 方式 |
---|---|
盈透证券 | ib-insync |
富途证券 | futu-api |
长桥证券 | longport |
老虎证券 | tigeropen |
本项目支持它们的原理是在函数中而非模块级引用相关 SDK 的包, 即使得这个项目没有要求必须安装这些依赖. 使用者需要使用到哪个平台, 需要自行在工程项目中安装其 SDK 方能正常工作, 也就是说使用者根据自身情况自行选择和解决 SDK 兼容性问题.
SDK客户端连通(ping)、交易相关功能:
交易通道 | ping | 可用资金 | 查持仓 | 下单 | 撤单 | 查订单 |
---|---|---|---|---|---|---|
盈透证券 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
富途证券 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
长桥证券 | - | ✅ | ✅ | ✅ | ✅ | ✅ |
老虎证券 | - | ✅ | ✅ | ✅ | ✅ | ✅ |
交易品种范围:
交易通道 | 美股正股 | 港股正股 | A股正股 |
---|---|---|---|
盈透证券 | ✅ | 不确定 | ❌ |
富途证券 | ✅ | 不确定 | ❌ |
长桥证券 | ✅ | 不确定 | ❌ |
老虎证券 | ✅ | 不确定 | ❌ |
市场、报价相关功能:
交易通道 | 市场状态 | 报价快照 |
---|---|---|
盈透证券 | ❌ | 不支持 |
富途证券 | ✅ | ✅ |
长桥证券 | ❌ | ✅ |
老虎证券 | ✅ | ✅ |
报价品种范围:
交易通道 | 美股正股 | 港股正股 | A股正股 |
---|---|---|---|
盈透证券 | ❌ | ❌ | ❌ |
富途证券 | ✅ | 不确定 | ✅ |
长桥证券 | ✅ | 不确定 | ✅ |
老虎证券 | ✅ | 不确定 | 不支持 |
项目以及使用到的框架以及实际的部署方式可能存在弱点和缺陷, 因此项目仅供学习参考,不为任何盈亏负有责任。
各个交易平台提供的接口服务, 并不能保证提供的数据准确、交易可靠以及有效, 参与投资请意识到技术上的风险隐患.
在配置交易通道前, 需要自行产生16到32字符的唯一实例id, 一个实例id对应一家交易通道配置. 实例id会包含在请求的 path 中, 防止接口地址被嗅探.
另外使用实例id是为了区分比如你有多家相同交易通道的账户, 这样如果用交易通道做类别就没办法多账户控制了.
除了实例id, 交易通道需要配置一组16到64字符token, 在请求中携带 HT-TOKEN
header 来确保可以正常操作此实例.
例如下面是一个启动脚本:
from httptrading import run, Futu
# 交易通道的参数
args = {
'host': '127.0.0.1',
'port': 8888,
'trade_env': 'SIMULATE',
}
futu = Futu(
broker_args=args,
instance_id='WyLqtMhDvAnBb6a3',
tokens=['Vt5UCW2sLBvgPXjR', ],
)
run(
host='0.0.0.0',
port=8080,
brokers=[futu, ],
)
这样携带某项 token 访问 http://127.0.0.1:8080/httptrading/api/WyLqtMhDvAnBb6a3/market/state 可以得到响应.
正常返回的接口会包含如下的字段:
{
"type": "apiResponse", // 节的类型
"instanceId": "ggUqPZbSKuQ7Ewsk", // 实例id
"broker": "futu", // 通道的类型
"brokerDisplay": "富途证券", // 通道的展示名称
"time": "2025-05-28T05:20:07.062298+00:00", // 服务器时间
"ex": null // 引发异常的文字描述
}
GET /httptrading/api/{instanceId}/ping/state
用于检测和修复 SDK 客户端的连接, 某些需要维持客户端连接的交易通道需要具体实现这个接口.
对于用户, 最好在操作交易通道前测试这个接口, 避免下单时出错引起更复杂的人工检查核对.
{
"type": "apiResponse",
"instanceId": "ggUqPZbSKuQ7Ewsk",
"broker": "futu",
"brokerDisplay": "富途证券",
"time": "2025-05-28T05:20:07.062298+00:00",
"ex": null,
"pong": true
}
如果 pong 是 false, 表明客户端测试是失败的.
GET /httptrading/api/{instanceId}/market/quote
需要在 query 提供 Contract 参数:
参数 | 说明 | 举例 |
---|---|---|
tradeType | 说明标的的品种 | Securities: 证券 |
symbol | 代码 | QQQ, 00700, 000001 |
region | 以国家区分代码 | US, HK, CN |
举例 ?tradeType=Securities®ion=CN&symbol=000001 参数的结果:
{
"type": "apiResponse",
"instanceId": "ggUqPZbSKuQ7Ewsk",
"broker": "futu",
"brokerDisplay": "富途证券",
"time": "2025-05-28T05:25:19.542635+00:00",
"ex": null,
"quote": {
"type": "quote",
"contract": {
"type": "contract",
"tradeType": "Securities",
"region": "CN",
"symbol": "000001"
},
"currency": "CNY", // 币种
"isTradable": true, // 此时是否可交易, 比如受到停牌熔断影响
"latest": 11.53, // 最新价
"preClose": 11.49, // 昨日收盘价
"highPrice": 11.55, // 日最高价
"lowPrice": 11.44, // 日最低价
"openPrice": 11.5, // 开盘价
"timestamp": 1748409918000 // 快照行情的时间
}
}
GET /httptrading/api/{instanceId}/market/state
{
"type": "apiResponse",
"instanceId": "ggUqPZbSKuQ7Ewsk",
"broker": "futu",
"brokerDisplay": "富途证券",
"time": "2025-05-28T05:33:42.543109+00:00",
"ex": null,
"marketStatus": {
"type": "marketStatusMap",
"securities": { // 证券类市场状态, 以 region 为键的结构
"US": {
"type": "marketStatus",
"region": "US",
"originStatus": "AFTER_HOURS_END", // 交易通道原始市场状态
"unifiedStatus": "CLOSED" // 统一映射的定义
},
"CN": {
"type": "marketStatus",
"region": "CN",
"originStatus": "AFTERNOON",
"unifiedStatus": "RTH"
},
"HK": {
"type": "marketStatus",
"region": "HK",
"originStatus": "AFTERNOON",
"unifiedStatus": "RTH"
}
}
}
}
统一映射的枚举:
枚举 | 说明 |
---|---|
UNKNOWN | 开盘前, 不能映射到的状态 |
OVERNIGHT | 夜盘 |
PRE_HOURS | 盘前 |
RTH | 正常交易时段 |
REST | 休市 |
AFTER_HOURS | 盘后 |
CLOSED | 收盘 |
GET /httptrading/api/{instanceId}/cash/state
{
"type": "apiResponse",
"instanceId": "ggUqPZbSKuQ7Ewsk",
"broker": "futu",
"brokerDisplay": "富途证券",
"time": "2025-05-28T05:39:10.743937+00:00",
"ex": null,
"cash": {
"type": "cash",
"currency": "USD",
"amount": 30877.499
}
}
GET /httptrading/api/{instanceId}/position/state
{
"type": "apiResponse",
"instanceId": "ggUqPZbSKuQ7Ewsk",
"broker": "futu",
"brokerDisplay": "富途证券",
"time": "2025-05-28T05:40:58.470655+00:00",
"ex": null,
"positions": [
{
"type": "position",
"broker": "futu",
"brokerDisplay": "富途证券",
"contract": {
"type": "contract",
"tradeType": "Securities",
"region": "US",
"symbol": "QQQ"
},
"unit": "Share",
"currency": "USD",
"qty": 2400
}
]
}
POST /httptrading/api/{instanceId}/order/place
需要提交 json 格式的 Body, 其中的参数如下:
参数 | 说明 | 举例 |
---|---|---|
tradeType | 说明标的的品种 | Securities: 证券 |
symbol | 代码 | QQQ, 00700, 000001 |
region | 以国家区分代码 | US, HK, CN |
price | 限价 | 市价单不填此项 |
qty | 订单数量, 整数 | |
orderType | 订单类型 | Limit: 限价单 Market: 市价单 |
timeInForce | 订单的有效期 | DAY: 日内 GTC: 撤销前有效 |
lifecycle | 订单交易时段 | RTH: 正常交易时段 ETH: 正常交易时段+盘前盘后 OVERNIGHT: 夜盘 |
direction | 买卖方向 | BUY, SELL |
例如 Body 可以是这样:
{
"tradeType": "Securities",
"symbol": "AAPL",
"region": "US",
"price": 200.00,
"qty": 12,
"orderType": "Limit",
"timeInForce": "DAY",
"lifecycle": "ETH",
"direction": "BUY"
}
产生的响应:
{
"type": "apiResponse",
"instanceId": "ggUqPZbSKuQ7Ewsk",
"broker": "futu",
"brokerDisplay": "富途证券",
"time": "2025-05-28T06:01:23.575968+00:00",
"ex": null,
"orderId": "69788888", // 订单号
"args": { // 传递的参数
"tradeType": "Securities",
"symbol": "AAPL",
"region": "US",
"price": 200,
"qty": 12,
"orderType": "Limit",
"timeInForce": "DAY",
"lifecycle": "ETH",
"direction": "BUY"
}
}
POST /httptrading/api/{instanceId}/order/cancel
需要提交 json 格式的 Body, 其中包含 orderId 字段来传递需要撤单的订单号码.
{
"type": "apiResponse",
"instanceId": "ggUqPZbSKuQ7Ewsk",
"broker": "futu",
"brokerDisplay": "富途证券",
"time": "2025-05-28T06:45:41.645855+00:00",
"ex": null // ex 字段将记录异常原因
}
⚠️ 撤单接口仅完成对交易通道的撤单接口调用, 不代表在较短时间后订单可以进入撤销的状态. 一个例子是假日发起撤单, 通道不一定执行撤单而是进入已请求撤单的状态.
GET /httptrading/api/{instanceId}/order/state?orderId={订单号}
接口会返回成交和订单状态相关的信息, 订单的更多信息, 需要在下单时自行保存.
{
"type": "apiResponse",
"instanceId": "ggUqPZbSKuQ7Ewsk",
"broker": "futu",
"brokerDisplay": "富途证券",
"time": "2025-05-28T05:59:29.984021+00:00",
"ex": null,
"order": {
"type": "order",
"orderId": "6278888",
"currency": "USD",
"qty": 12, // 订单数量
"filledQty": 0, // 已成交数量
"avgPrice": 0, // 成交价
"errorReason": "", // 如果订单异常, 这里记录错误信息
"isCanceled": false, // 是否已撤销
"isFilled": false, // 是否全部成交
"isCompleted": false, // 全部成交 或者 有订单异常 或者 已撤销
"isCancelable": true // 是否可撤的标志, 等价于 not isCompleted and not isPendingCancel
}
}
交易通道 | 支持查单个订单 |
---|---|
盈透证券 | ❌ |
富途证券 | ❌ |
长桥证券 | ✅ |
老虎证券 | ✅ |
import httptrading
args = {
'host': '127.0.0.1', # OpenD 地址
'port': 12345, # OpenD 地址
'trade_env': 'REAL', # 实盘填REAL, 模拟盘填SIMULATE
'pk_path': '', # OpenD非本机部署需要填证书路径
# 实盘操作订单需要解锁密码
# https://openapi.futunn.com/futu-api-doc/trade/unlock.html
# 这里储存密码的md5值, 使用命令
# macos: md5 -s "123456"
# linux: echo -n "123456" | md5sum
'unlock_pin': '',
}
broker = httptrading.Futu(args, ...)
import httptrading
args = {
'app_key': '...',
'app_secret': '...',
# 需要制作一个 token 文件供服务读写
# 具体情况见 httptrading/broker/longbridge.py
'token_file': '...',
# 是否尝试自动刷新即将过期的 token
'auto_refresh_token': True,
}
broker = httptrading.LongBridge(args, ...)
import httptrading
args = {
'tiger_id': '...',
'account': '...',
'pk_path': '...',
}
broker = httptrading.Tiger(args, ...)
import httptrading
args = {
'host': '127.0.0.1',
'port': 1234,
'timeout': 8,
'client_id': 123456, # TWS中设置的号码
'account_id': 'U...', # U开头的账户号
}
broker = httptrading.InteractiveBrokers(args, ...)
from httptrading import *
from httptrading.model import *
@broker_register('myBroker', 'XX证券')
class MyBroker(BaseBroker):
# 根据需要的功能实现接口
# 如果 sdk 提供的方式会阻塞 eventloop, 需要使用 self.call_sync 方法传入阻塞方法
async def place_order(
self,
contract: Contract,
order_type: OrderType,
time_in_force: TimeInForce,
lifecycle: Lifecycle,
direction: str,
qty: int,
price: float = None,
**kwargs
) -> str:
raise NotImplementedError
async def order(self, order_id: str) -> Order:
raise NotImplementedError
async def cancel_order(self, order_id: str):
raise NotImplementedError
async def positions(self) -> list[Position]:
raise NotImplementedError
async def cash(self) -> Cash:
raise NotImplementedError
async def ping(self) -> bool:
return True
async def quote(self, contract: Contract) -> Quote:
raise NotImplementedError
async def market_status(self) -> dict[TradeType, dict[str, MarketStatus] | str]:
raise NotImplementedError
import aiohttp.web
import httptrading
class MyApi(httptrading.HttpTradingView):
async def get(self):
broker = self.current_broker()
return self.response_api(
broker=broker,
args={
'method': 'GET',
'hello': 'world',
},
)
async def post(self):
body_d: dict = await self.request.json()
broker = self.current_broker()
return self.response_api(
broker=broker,
args={
'method': 'POST',
'hello': 'world',
'body': body_d,
},
)
httptrading.run(
host='127.0.0.1',
port=8080,
brokers=list(),
extend_apis=[aiohttp.web.view(r'/httptrading/api/{instance_id:\w{16,32}}/hello/world', MyApi), ],
)
import httptrading
def my_std_apis():
std_apis = httptrading.std_api_factory()
apis = [api for api in std_apis if 1 == 1]
return apis
httptrading.run(
host='127.0.0.1',
port=8080,
brokers=list(),
std_apis=my_std_apis,
)