├── .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 |
--------------------------------------------------------------------------------