├── .flake8 ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── script └── run.py ├── setup.cfg ├── setup.py └── vnpy_futu ├── __init__.py └── futu_gateway.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = venv,build,__pycache__,__init__.py,ib,talib,uic 3 | ignore = 4 | E501 line too long, fixed by black 5 | W503 line break before binary operator 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.0.1版本 2 | 3 | 1. 使用zoneinfo替换pytz库 4 | 2. 调整接口初始化时,接口名称的赋值方式 5 | 3. 调整安装脚本setup.cfg,添加Python版本限制 6 | 4. 将查询资金持仓的interval从1调整为3 7 | 5. 增加期货支持 8 | 6. 增加历史数据查询支持 9 | 7. 增加orderbook推送过滤逻辑 10 | 8. 基于最新版本调整委托状态的枚举值 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 vn.py 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vn.py框架的富途证券交易接口 2 | 3 |

4 | 5 |

6 | 7 |

8 | 9 | 10 | 11 | 12 |

13 | 14 | ## 说明 15 | 16 | 基于futu-api开发的富途证券港股、期货以及美股交易接口。使用时需要注意本接口只支持限价单。 17 | 18 | ## 安装 19 | 20 | 安装需要基于3.4.0版本以上的[VN Studio](https://www.vnpy.com)。 21 | 22 | 直接使用pip命令: 23 | 24 | ``` 25 | pip install vnpy_futu 26 | ``` 27 | 28 | 下载解压后在cmd中运行: 29 | 30 | ``` 31 | python setup.py install 32 | ``` 33 | 34 | ## 使用 35 | 36 | 以脚本方式启动(script/run.py): 37 | 38 | ``` 39 | from vnpy.event import EventEngine 40 | from vnpy.trader.engine import MainEngine 41 | from vnpy.trader.ui import MainWindow, create_qapp 42 | 43 | from vnpy_futu import FutuGateway 44 | 45 | 46 | def main(): 47 | """主入口函数""" 48 | qapp = create_qapp() 49 | 50 | event_engine = EventEngine() 51 | main_engine = MainEngine(event_engine) 52 | main_engine.add_gateway(FutuGateway) 53 | 54 | main_window = MainWindow(main_engine, event_engine) 55 | main_window.showMaximized() 56 | 57 | qapp.exec() 58 | 59 | 60 | if __name__ == "__main__": 61 | main() 62 | ``` 63 | -------------------------------------------------------------------------------- /script/run.py: -------------------------------------------------------------------------------- 1 | from vnpy.event import EventEngine 2 | from vnpy.trader.engine import MainEngine 3 | from vnpy.trader.ui import MainWindow, create_qapp 4 | 5 | from vnpy_futu import FutuGateway 6 | 7 | 8 | def main(): 9 | """主入口函数""" 10 | qapp = create_qapp() 11 | 12 | event_engine = EventEngine() 13 | main_engine = MainEngine(event_engine) 14 | main_engine.add_gateway(FutuGateway) 15 | 16 | main_window = MainWindow(main_engine, event_engine) 17 | main_window.showMaximized() 18 | 19 | qapp.exec() 20 | 21 | 22 | if __name__ == "__main__": 23 | main() 24 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = vnpy_futu 3 | version = 6.3.2808.0 4 | url = https://www.vnpy.com 5 | license = MIT 6 | author = Xiaoyou Chen 7 | author_email = xiaoyou.chen@mail.vnpy.com 8 | description = FUTU gateway for vn.py quant trading framework. 9 | long_description = file: README.md 10 | long_description_content_type = text/markdown 11 | keywords = 12 | quant 13 | quantitative 14 | investment 15 | trading 16 | algotrading 17 | classifiers = 18 | Development Status :: 5 - Production/Stable 19 | Operating System :: OS Independent 20 | Programming Language :: Python :: 3 21 | Programming Language :: Python :: 3.7 22 | Programming Language :: Python :: 3.8 23 | Programming Language :: Python :: 3.9 24 | Programming Language :: Python :: 3.10 25 | Topic :: Office/Business :: Financial :: Investment 26 | Programming Language :: Python :: Implementation :: CPython 27 | License :: OSI Approved :: MIT License 28 | Natural Language :: Chinese (Simplified) 29 | 30 | [options] 31 | packages = find: 32 | zip_safe = False 33 | python_requires = >=3.7 34 | install_requires = 35 | futu-api 36 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | 4 | setup() 5 | -------------------------------------------------------------------------------- /vnpy_futu/__init__.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2021 vn.py 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import importlib_metadata 24 | 25 | from .futu_gateway import FutuGateway 26 | 27 | 28 | try: 29 | __version__ = importlib_metadata.version("vnpy_futu") 30 | except importlib_metadata.PackageNotFoundError: 31 | __version__ = "dev" -------------------------------------------------------------------------------- /vnpy_futu/futu_gateway.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from copy import copy 3 | from datetime import datetime 4 | from threading import Thread 5 | from time import sleep 6 | from typing import Any, Dict, List, Set, Tuple, Union 7 | 8 | from futu import ( 9 | ModifyOrderOp, 10 | TrdSide, 11 | TrdEnv, 12 | TrdMarket, 13 | KLType, 14 | OpenQuoteContext, 15 | OrderBookHandlerBase, 16 | OrderStatus, 17 | OrderType, 18 | RET_ERROR, 19 | RET_OK, 20 | StockQuoteHandlerBase, 21 | TradeDealHandlerBase, 22 | TradeOrderHandlerBase, 23 | OpenSecTradeContext, 24 | OpenFutureTradeContext 25 | ) 26 | 27 | from vnpy.event import EventEngine 28 | from vnpy.trader.constant import ( 29 | Direction, 30 | Exchange, 31 | Offset, 32 | Product, 33 | Status, 34 | Interval 35 | ) 36 | from vnpy.trader.gateway import BaseGateway 37 | from vnpy.trader.object import ( 38 | TickData, 39 | OrderData, 40 | TradeData, 41 | BarData, 42 | AccountData, 43 | ContractData, 44 | PositionData, 45 | SubscribeRequest, 46 | OrderRequest, 47 | CancelRequest, 48 | HistoryRequest 49 | ) 50 | from vnpy.trader.event import EVENT_TIMER 51 | from vnpy.trader.utility import ZoneInfo 52 | 53 | 54 | # 委托状态映射 55 | STATUS_FUTU2VT: Dict[OrderStatus, Status] = { 56 | OrderStatus.NONE: Status.SUBMITTING, 57 | OrderStatus.WAITING_SUBMIT: Status.SUBMITTING, 58 | OrderStatus.SUBMITTING: Status.SUBMITTING, 59 | OrderStatus.SUBMITTED: Status.NOTTRADED, 60 | OrderStatus.FILLED_PART: Status.PARTTRADED, 61 | OrderStatus.FILLED_ALL: Status.ALLTRADED, 62 | OrderStatus.CANCELLED_PART: Status.CANCELLED, 63 | OrderStatus.CANCELLED_ALL: Status.CANCELLED, 64 | OrderStatus.FAILED: Status.REJECTED, 65 | OrderStatus.DISABLED: Status.CANCELLED, 66 | OrderStatus.DELETED: Status.CANCELLED, 67 | } 68 | 69 | # 多空方向映射 70 | DIRECTION_VT2FUTU: Dict[Direction, TrdSide] = { 71 | Direction.LONG: TrdSide.BUY, 72 | Direction.SHORT: TrdSide.SELL, 73 | } 74 | DIRECTION_FUTU2VT: Dict[TrdSide, Tuple] = { 75 | TrdSide.BUY: (Direction.LONG, Offset.OPEN), 76 | TrdSide.SELL: (Direction.SHORT, Offset.OPEN), 77 | TrdSide.BUY_BACK: (Direction.LONG, Offset.CLOSE), 78 | TrdSide.SELL_SHORT: (Direction.SHORT, Offset.CLOSE), 79 | } 80 | 81 | # 交易所映射 82 | EXCHANGE_VT2FUTU: Dict[Exchange, str] = { 83 | Exchange.SMART: "US", 84 | Exchange.SEHK: "HK", 85 | Exchange.HKFE: "HK_FUTURE", 86 | } 87 | EXCHANGE_FUTU2VT: Dict[str, Exchange] = {v: k for k, v in EXCHANGE_VT2FUTU.items()} 88 | 89 | # 产品类型映射 90 | PRODUCT_VT2FUTU: Dict[Product, str] = { 91 | Product.EQUITY: "STOCK", 92 | Product.INDEX: "IDX", 93 | Product.ETF: "ETF", 94 | Product.WARRANT: "WARRANT", 95 | Product.BOND: "BOND", 96 | Product.FUTURES: "FUTURE" 97 | } 98 | 99 | 100 | # 其他常量 101 | CHINA_TZ = ZoneInfo("Asia/Shanghai") 102 | 103 | 104 | class FutuGateway(BaseGateway): 105 | """ 106 | veighna用于对接富途证券的交易接口。 107 | """ 108 | default_name: str = "FUTU" 109 | 110 | default_setting: Dict[str, Any] = { 111 | "密码": "", 112 | "地址": "127.0.0.1", 113 | "端口": 11111, 114 | "市场": ["HK", "US", "HK_FUTURE"], 115 | "环境": [TrdEnv.REAL, TrdEnv.SIMULATE], 116 | } 117 | 118 | exchanges: List[str] = list(EXCHANGE_FUTU2VT.values()) 119 | 120 | def __init__(self, event_engine: EventEngine, gateway_name: str) -> None: 121 | """构造函数""" 122 | super().__init__(event_engine, gateway_name) 123 | 124 | self.quote_ctx: OpenQuoteContext = None 125 | self.trade_ctx: Union[OpenSecTradeContext, OpenFutureTradeContext] = None 126 | 127 | self.host: str = "" 128 | self.port: int = 0 129 | self.market: str = "" 130 | self.password: str = "" 131 | self.env: TrdEnv = TrdEnv.SIMULATE 132 | 133 | self.ticks: Dict[str, TickData] = {} 134 | self.trades: Set = set() 135 | self.contracts: Dict[str, ContractData] = {} 136 | 137 | self.thread: Thread = Thread(target=self.query_data) 138 | 139 | self.count: int = 0 140 | self.interval: int = 3 141 | self.query_funcs: list = [self.query_account, self.query_position] 142 | 143 | def connect(self, setting: dict) -> None: 144 | """连接交易接口""" 145 | self.host: str = setting["地址"] 146 | self.port: int = setting["端口"] 147 | self.market: str = setting["市场"] 148 | self.password: str = setting["密码"] 149 | self.env: TrdEnv = setting["环境"] 150 | 151 | self.connect_quote() 152 | self.connect_trade() 153 | 154 | self.thread.start() 155 | 156 | def query_data(self) -> None: 157 | """查询数据""" 158 | sleep(2.0) # 等待两秒直到连接成功 159 | 160 | self.query_contract() 161 | self.query_trade() 162 | self.query_order() 163 | self.query_position() 164 | self.query_account() 165 | 166 | # 初始化定时查询任务 167 | self.event_engine.register(EVENT_TIMER, self.process_timer_event) 168 | 169 | def process_timer_event(self, event) -> None: 170 | """定时事件处理""" 171 | self.count += 1 172 | if self.count < self.interval: 173 | return 174 | self.count = 0 175 | func = self.query_funcs.pop(0) 176 | func() 177 | self.query_funcs.append(func) 178 | 179 | def connect_quote(self) -> None: 180 | """连接行情服务端""" 181 | self.quote_ctx: OpenQuoteContext = OpenQuoteContext(self.host, self.port) 182 | 183 | class QuoteHandler(StockQuoteHandlerBase): 184 | gateway: FutuGateway = self 185 | 186 | def on_recv_rsp(self, rsp_str): 187 | ret_code, content = super(QuoteHandler, self).on_recv_rsp( 188 | rsp_str 189 | ) 190 | if ret_code != RET_OK: 191 | return RET_ERROR, content 192 | self.gateway.process_quote(content) 193 | return RET_OK, content 194 | 195 | class OrderBookHandler(OrderBookHandlerBase): 196 | gateway: FutuGateway = self 197 | 198 | def on_recv_rsp(self, rsp_str): 199 | ret_code, content = super(OrderBookHandler, self).on_recv_rsp( 200 | rsp_str 201 | ) 202 | if ret_code != RET_OK: 203 | return RET_ERROR, content 204 | self.gateway.process_orderbook(content) 205 | return RET_OK, content 206 | 207 | self.quote_ctx.set_handler(QuoteHandler()) 208 | self.quote_ctx.set_handler(OrderBookHandler()) 209 | self.quote_ctx.start() 210 | 211 | self.write_log("行情接口连接成功") 212 | 213 | def connect_trade(self) -> None: 214 | """连接交易服务端""" 215 | if self.market == "HK": 216 | self.trade_ctx = OpenSecTradeContext(filter_trdmarket=TrdMarket.HK, host=self.host, port=self.port,) 217 | elif self.market == "US": 218 | self.trade_ctx = OpenSecTradeContext(filter_trdmarket=TrdMarket.US, host=self.host, port=self.port,) 219 | elif self.market == "HK_FUTURE": 220 | self.trade_ctx = OpenFutureTradeContext(host=self.host, port=self.port) 221 | 222 | class OrderHandler(TradeOrderHandlerBase): 223 | gateway: FutuGateway = self 224 | 225 | def on_recv_rsp(self, rsp_str): 226 | ret_code, content = super(OrderHandler, self).on_recv_rsp( 227 | rsp_str 228 | ) 229 | if ret_code != RET_OK: 230 | return RET_ERROR, content 231 | self.gateway.process_order(content) 232 | return RET_OK, content 233 | 234 | class DealHandler(TradeDealHandlerBase): 235 | gateway: FutuGateway = self 236 | 237 | def on_recv_rsp(self, rsp_str): 238 | ret_code, content = super(DealHandler, self).on_recv_rsp( 239 | rsp_str 240 | ) 241 | if ret_code != RET_OK: 242 | return RET_ERROR, content 243 | self.gateway.process_deal(content) 244 | return RET_OK, content 245 | 246 | # 交易接口解锁 247 | code, data = self.trade_ctx.unlock_trade(self.password) 248 | if code == RET_OK: 249 | self.write_log("交易接口解锁成功") 250 | else: 251 | self.write_log(f"交易接口解锁失败,原因:{data}") 252 | 253 | # 连接交易接口 254 | self.trade_ctx.set_handler(OrderHandler()) 255 | self.trade_ctx.set_handler(DealHandler()) 256 | self.trade_ctx.start() 257 | self.write_log("交易接口连接成功") 258 | 259 | def subscribe(self, req: SubscribeRequest) -> None: 260 | """订阅行情""" 261 | for data_type in ["QUOTE", "ORDER_BOOK"]: 262 | futu_symbol: str = convert_symbol_vt2futu(req.symbol, req.exchange) 263 | code, data = self.quote_ctx.subscribe(futu_symbol, data_type, True) 264 | 265 | if code: 266 | self.write_log(f"订阅行情失败:{data}") 267 | 268 | def send_order(self, req: OrderRequest) -> str: 269 | """委托下单""" 270 | side: TrdSide = DIRECTION_VT2FUTU[req.direction] 271 | futu_order_type: OrderType = OrderType.NORMAL # 只支持限价单 272 | 273 | # 设置调整价格限制 274 | if req.direction is Direction.LONG: 275 | adjust_limit: float = 0.05 276 | else: 277 | adjust_limit: float = -0.05 278 | 279 | futu_symbol: str = convert_symbol_vt2futu(req.symbol, req.exchange) 280 | code, data = self.trade_ctx.place_order( 281 | req.price, 282 | req.volume, 283 | futu_symbol, 284 | side, 285 | futu_order_type, 286 | trd_env=self.env, 287 | adjust_limit=adjust_limit, 288 | ) 289 | 290 | if code: 291 | self.write_log(f"委托失败:{data}") 292 | return "" 293 | 294 | for ix, row in data.iterrows(): 295 | orderid: str = str(row["order_id"]) 296 | 297 | order: OrderData = req.create_order_data(orderid, self.gateway_name) 298 | self.on_order(order) 299 | return order.vt_orderid 300 | 301 | def cancel_order(self, req: CancelRequest) -> None: 302 | """委托撤单""" 303 | code, data = self.trade_ctx.modify_order( 304 | ModifyOrderOp.CANCEL, req.orderid, 0, 0, trd_env=self.env 305 | ) 306 | 307 | if code: 308 | self.write_log(f"撤单失败:{data}") 309 | 310 | def query_contract(self) -> None: 311 | """查询合约""" 312 | # get_stock_basicinfo 是没有区分future的, 区分了地区 313 | if self.market in ["HK", "HK_FUTURE"]: 314 | market = "HK" 315 | else: 316 | market = self.market 317 | 318 | for product, futu_product in PRODUCT_VT2FUTU.items(): 319 | code, data = self.quote_ctx.get_stock_basicinfo( 320 | market, futu_product 321 | ) 322 | 323 | if code: 324 | self.write_log(f"查询合约信息失败:{data}") 325 | return 326 | 327 | for ix, row in data.iterrows(): 328 | symbol, exchange = convert_symbol_futu2vt(row["code"]) 329 | contract: ContractData = ContractData( 330 | symbol=symbol, 331 | exchange=exchange, 332 | name=row["name"], 333 | product=product, 334 | size=1, 335 | pricetick=0.001, 336 | history_data=True, 337 | net_position=True, 338 | gateway_name=self.gateway_name, 339 | ) 340 | self.on_contract(contract) 341 | self.contracts[contract.vt_symbol] = contract 342 | 343 | self.write_log("合约信息查询成功") 344 | 345 | def query_account(self) -> None: 346 | """查询资金""" 347 | code, data = self.trade_ctx.accinfo_query(trd_env=self.env, acc_id=0) 348 | 349 | if code: 350 | self.write_log(f"查询账户资金失败:{data}") 351 | return 352 | 353 | for ix, row in data.iterrows(): 354 | account: AccountData = AccountData( 355 | accountid=f"{self.gateway_name}_{self.market}", 356 | balance=float(row["total_assets"]), 357 | frozen=(float(row["total_assets"]) - float(row["avl_withdrawal_cash"])), 358 | gateway_name=self.gateway_name, 359 | ) 360 | self.on_account(account) 361 | 362 | def query_position(self) -> None: 363 | """查询持仓""" 364 | code, data = self.trade_ctx.position_list_query( 365 | trd_env=self.env, acc_id=0 366 | ) 367 | 368 | if code: 369 | self.write_log(f"查询持仓失败:{data}") 370 | return 371 | 372 | for ix, row in data.iterrows(): 373 | symbol, exchange = convert_symbol_futu2vt(row["code"]) 374 | pos: PositionData = PositionData( 375 | symbol=symbol, 376 | exchange=exchange, 377 | direction=Direction.NET, 378 | volume=row["qty"], 379 | frozen=(float(row["qty"]) - float(row["can_sell_qty"])), 380 | price=float(row["cost_price"]), 381 | pnl=float(row["pl_val"]), 382 | gateway_name=self.gateway_name, 383 | ) 384 | 385 | self.on_position(pos) 386 | 387 | def query_order(self) -> None: 388 | """查询未成交委托""" 389 | code, data = self.trade_ctx.order_list_query("", trd_env=self.env) 390 | 391 | if code: 392 | self.write_log(f"查询委托失败:{data}") 393 | return 394 | 395 | self.process_order(data) 396 | self.write_log("委托查询成功") 397 | 398 | def query_trade(self) -> None: 399 | """查询成交""" 400 | code, data = self.trade_ctx.deal_list_query("", trd_env=self.env) 401 | 402 | if code: 403 | self.write_log(f"查询成交失败:{data}") 404 | return 405 | 406 | self.process_deal(data) 407 | self.write_log("成交查询成功") 408 | 409 | def close(self) -> None: 410 | """关闭接口""" 411 | if self.quote_ctx: 412 | self.quote_ctx.close() 413 | 414 | if self.trade_ctx: 415 | self.trade_ctx.close() 416 | 417 | def get_tick(self, code) -> TickData: 418 | """查询Tick数据""" 419 | tick: TickData = self.ticks.get(code, None) 420 | symbol, exchange = convert_symbol_futu2vt(code) 421 | if not tick: 422 | tick: TickData = TickData( 423 | symbol=symbol, 424 | exchange=exchange, 425 | datetime=datetime.now(CHINA_TZ), 426 | gateway_name=self.gateway_name, 427 | ) 428 | self.ticks[code] = tick 429 | 430 | contract: ContractData = self.contracts.get(tick.vt_symbol, None) 431 | if contract: 432 | tick.name = contract.name 433 | 434 | return tick 435 | 436 | def query_history(self, req: HistoryRequest) -> List[BarData]: 437 | """查询历史数据""" 438 | bars: List[BarData] = [] 439 | 440 | if req.interval != Interval.MINUTE: 441 | self.write_log(f"获取K线数据失败,FUTU接口暂不提供{req.interval.value}级别历史数据") 442 | return bars 443 | 444 | symbol: str = convert_symbol_vt2futu(req.symbol, req.exchange) 445 | start_date: str = req.start.replace(tzinfo=None).strftime("%Y-%m-%d") 446 | end_date: str = req.end.replace(tzinfo=None).strftime("%Y-%m-%d %H:%M:%S") 447 | 448 | ret, history_df, page_req_key = self.quote_ctx.request_history_kline(code=symbol, start=start_date, end=end_date, ktype=KLType.K_1M) # 每页5个,请求第一页 449 | if ret != RET_OK: 450 | self.write_log(f"获取K线数据失败,原因:{history_df}") 451 | return bars 452 | 453 | while page_req_key != None: # 请求后面的所有结果 454 | ret, data, page_req_key = self.quote_ctx.request_history_kline(code=symbol, start=start_date, end=end_date, ktype=KLType.K_1M, page_req_key=page_req_key) # 请求翻页后的数据 455 | if ret == RET_OK: 456 | history_df = history_df.append(data, ignore_index=True) 457 | else: 458 | self.write_log(f"{data}") 459 | 460 | history_df["time_key"] = pd.to_datetime(history_df["time_key"]) 461 | history_df["time_key"] = history_df["time_key"] - pd.Timedelta(1, "m") 462 | history_df["time_key"] = history_df["time_key"].dt.strftime("%Y-%m-%d %H:%M:%S") 463 | 464 | for ix, row in history_df.iterrows(): 465 | bar: BarData = BarData( 466 | gateway_name=self.gateway_name, 467 | symbol=req.symbol, 468 | exchange=req.exchange, 469 | datetime=generate_datetime(row["time_key"]), 470 | interval=Interval.MINUTE, 471 | volume=row["volume"], 472 | turnover=row["turnover"], 473 | open_interest=0, 474 | open_price=row["open"], 475 | high_price=row["high"], 476 | low_price=row["low"], 477 | close_price=row["close"] 478 | ) 479 | bars.append(bar) 480 | 481 | return bars 482 | 483 | def process_quote(self, data) -> None: 484 | """报价推送""" 485 | for ix, row in data.iterrows(): 486 | symbol: str = row["code"] 487 | 488 | date: str = row["data_date"].replace("-", "") 489 | time: str = row["data_time"] 490 | dt: datetime = datetime.strptime(f"{date} {time}", "%Y%m%d %H:%M:%S") 491 | dt: datetime = dt.replace(tzinfo=CHINA_TZ) 492 | 493 | tick: TickData = self.get_tick(symbol) 494 | tick.datetime = dt 495 | tick.open_price = row["open_price"] 496 | tick.high_price = row["high_price"] 497 | tick.low_price = row["low_price"] 498 | tick.pre_close = row["prev_close_price"] 499 | tick.last_price = row["last_price"] 500 | tick.volume = row["volume"] 501 | 502 | if "price_spread" in row: 503 | spread = row["price_spread"] 504 | tick.limit_up = tick.last_price + spread * 10 505 | tick.limit_down = tick.last_price - spread * 10 506 | 507 | self.on_tick(copy(tick)) 508 | 509 | def process_orderbook(self, data) -> None: 510 | """行情信息处理推送""" 511 | symbol: str = data["code"] 512 | tick: TickData = self.get_tick(symbol) 513 | 514 | d: dict = tick.__dict__ 515 | if len(data) < 5: 516 | return 517 | 518 | for i in range(5): 519 | bid_data = data["Bid"][i] 520 | ask_data = data["Ask"][i] 521 | n = i + 1 522 | 523 | d["bid_price_%s" % n] = bid_data[0] 524 | d["bid_volume_%s" % n] = bid_data[1] 525 | d["ask_price_%s" % n] = ask_data[0] 526 | d["ask_volume_%s" % n] = ask_data[1] 527 | 528 | if tick.datetime: 529 | self.on_tick(copy(tick)) 530 | 531 | def process_order(self, data) -> None: 532 | """委托信息处理推送""" 533 | for ix, row in data.iterrows(): 534 | if row["order_status"] == OrderStatus.DELETED: 535 | continue 536 | 537 | direction, offset = DIRECTION_FUTU2VT[row["trd_side"]] 538 | symbol, exchange = convert_symbol_futu2vt(row["code"]) 539 | order: OrderData = OrderData( 540 | symbol=symbol, 541 | exchange=exchange, 542 | orderid=str(row["order_id"]), 543 | direction=direction, 544 | offset=offset, 545 | price=float(row["price"]), 546 | volume=row["qty"], 547 | traded=row["dealt_qty"], 548 | status=STATUS_FUTU2VT[row["order_status"]], 549 | datetime=generate_datetime(row["create_time"]), 550 | gateway_name=self.gateway_name, 551 | ) 552 | 553 | self.on_order(order) 554 | 555 | def process_deal(self, data) -> None: 556 | """成交信息处理推送""" 557 | for ix, row in data.iterrows(): 558 | tradeid: str = str(row["deal_id"]) 559 | if tradeid in self.trades: 560 | continue 561 | self.trades.add(tradeid) 562 | 563 | direction, offset = DIRECTION_FUTU2VT[row["trd_side"]] 564 | symbol, exchange = convert_symbol_futu2vt(row["code"]) 565 | trade: TradeData = TradeData( 566 | symbol=symbol, 567 | exchange=exchange, 568 | direction=direction, 569 | offset=offset, 570 | tradeid=tradeid, 571 | orderid=row["order_id"], 572 | price=float(row["price"]), 573 | volume=row["qty"], 574 | datetime=generate_datetime(row["create_time"]), 575 | gateway_name=self.gateway_name, 576 | ) 577 | 578 | self.on_trade(trade) 579 | 580 | 581 | def convert_symbol_futu2vt(code) -> str: 582 | """富途合约名称转换""" 583 | code_list = code.split(".") 584 | futu_exchange = code_list[0] 585 | futu_symbol = ".".join(code_list[1:]) 586 | exchange = EXCHANGE_FUTU2VT[futu_exchange] 587 | return futu_symbol, exchange 588 | 589 | 590 | def convert_symbol_vt2futu(symbol, exchange) -> str: 591 | """veighna合约名称转换""" 592 | futu_exchange: Exchange = EXCHANGE_VT2FUTU[exchange] 593 | return f"{futu_exchange}.{symbol}" 594 | 595 | 596 | def generate_datetime(s: str) -> datetime: 597 | """生成时间戳""" 598 | if "." in s: 599 | dt: datetime = datetime.strptime(s, "%Y-%m-%d %H:%M:%S.%f") 600 | else: 601 | dt: datetime = datetime.strptime(s, "%Y-%m-%d %H:%M:%S") 602 | 603 | dt: datetime = dt.replace(tzinfo=CHINA_TZ) 604 | return dt 605 | --------------------------------------------------------------------------------