├── .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
└── xtdata_demo.ipynb
└── vnpy_xt
├── __init__.py
├── xt_config.py
├── xt_datafeed.py
└── xt_gateway.py
/.github/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # 行为准则
2 |
3 | 这是一份VeighNa项目社区的行为准则,也是项目作者自己在刚入行量化金融行业时对于理想中的社区的期望:
4 |
5 | * 为交易员而生:作为一款从金融机构量化业务中诞生的交易系统开发框架,设计上都优先满足机构专业交易员的使用习惯,而不是其他用户(散户、爱好者、技术人员等)
6 |
7 | * 对新用户友好,保持耐心:大部分人在接触新东西的时候都是磕磕碰碰、有很多的问题,请记住此时别人对你伸出的援助之手,并把它传递给未来需要的人
8 |
9 | * 尊重他人,慎重言行:礼貌文明的交流方式除了能得到别人同样的回应,更能减少不必要的摩擦,保证高效的交流
10 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## 环境
2 |
3 | * 操作系统: 如Windows 11或者Ubuntu 22.04
4 | * Python版本: 如VeighNa Studio-4.0.0
5 | * VeighNa版本: 如v4.0.0发行版或者dev branch 20250320(下载日期)
6 |
7 | ## Issue类型
8 | 三选一:Bug/Enhancement/Question
9 |
10 | ## 预期程序行为
11 |
12 |
13 | ## 实际程序行为
14 |
15 |
16 | ## 重现步骤
17 |
18 | 针对Bug类型Issue,请提供具体重现步骤以及报错截图
19 |
20 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | 建议每次发起的PR内容尽可能精简,复杂的修改请拆分为多次PR,便于管理合并。
2 |
3 | ## 改进内容
4 |
5 | 1.
6 | 2.
7 | 3.
8 |
9 | ## 相关的Issue号(如有)
10 |
11 | Close #
--------------------------------------------------------------------------------
/.github/SUPPORT.md:
--------------------------------------------------------------------------------
1 | # 获取帮助
2 |
3 | 在开发和使用VeighNa项目的过程中遇到问题时,获取帮助的渠道包括:
4 |
5 | * Github Issues:[Issues页面](https://github.com/vnpy/vnpy/issues)
6 | * 官方QQ群: 262656087
7 | * 项目论坛:[VeighNa量化社区](http://www.vnpy.com/forum)
8 | * 项目邮箱: vn.py@foxmail.com
9 |
--------------------------------------------------------------------------------
/.github/workflows/pythonapp.yml:
--------------------------------------------------------------------------------
1 | name: Python application
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: windows-latest
9 |
10 | steps:
11 | - uses: actions/checkout@v1
12 | - name: Set up Python 3.13
13 | uses: actions/setup-python@v1
14 | with:
15 | python-version: '3.13'
16 | - name: Install dependencies
17 | run: |
18 | python -m pip install --upgrade pip
19 | pip install ta-lib==0.6.3 --index=https://pypi.vnpy.com
20 | pip install vnpy ruff mypy uv
21 | - name: Lint with ruff
22 | run: |
23 | # Run ruff linter based on pyproject.toml configuration
24 | ruff check .
25 | - name: Type check with mypy
26 | run: |
27 | # Run mypy type checking based on pyproject.toml configuration
28 | mypy vnpy_xt
29 | - name: Build packages with uv
30 | run: |
31 | # Build source distribution and wheel distribution
32 | uv build
33 |
--------------------------------------------------------------------------------
/.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 | .vscode/settings.json
131 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 1.4.4
2 |
3 | 1. 修复开盘集合竞价K线的价格没有反映到第一分钟最高价和最低价的问题
4 |
5 | # 1.4.3
6 |
7 | 1. XtGateway限制仅连接VIP服务器地址:115.231.218.73:55310, 115.231.218.79:55310
8 |
9 | # 1.4.2
10 |
11 | 1. 放宽filelock依赖库版本,解决兼容性问题
12 |
13 | # 1.4.1
14 |
15 | 1. 增加行情数据中的市场收盘状态推送
16 |
17 | # 1.4.0
18 |
19 | 1. 委托时仅对期权委托检查开平方向
20 | 2. 现货委托自动移除开平方向,保证底层兼容
21 |
22 | # 1.3.0
23 |
24 | 1. vnpy框架4.0版本升级适配
25 |
26 | # 1.2.6
27 |
28 | 1. 增加集合竞价K线volume为0时的过滤
29 |
30 | # 1.2.5
31 |
32 | 1. XtDatafeed限制仅连接VIP服务器地址:115.231.218.73:55310, 115.231.218.79:55310
33 |
34 | # 1.2.4
35 |
36 | 1. 增加实时行情中的涨跌停价字段
37 |
38 | # 1.2.3
39 |
40 | 1. 修复期权合约数据的gateway_name固定为XT的问题
41 |
42 | # 1.2.2
43 |
44 | 1. 升级240920.1.2版本API
45 | 2. XtGateway增加仿真交易功能,支持股票和股票期权
46 |
47 | # 1.2.1
48 |
49 | 1. 修复沪深非期权行情订阅的问题
50 |
51 | # 1.2.0
52 |
53 | 1. 增加实时行情接口XtGateway
54 |
55 | # 1.1.0
56 |
57 | 1. 更新底层xtquant到240613.1.1版本
58 | 2. 增加期货历史数据集合竞价K线合成支持
59 | 3. 开启使用期货真实夜盘时间
60 | 4. 基于文件锁实现xtdc单例运行
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015-present, Xiaoyou Chen
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 | # VeighNa框架的迅投研数据服务接口
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | ## 说明
15 |
16 | 基于迅投XtQuant封装开发的实时行情和数据服务接口,支持以下中国金融市场的K线和Tick数据:
17 |
18 | * 股票、基金、债券、ETF期权:
19 | * SSE:上海证券交易所
20 | * SZSE:深圳证券交易所
21 | * 期货、期货期权:
22 | * CFFEX:中国金融期货交易所
23 | * SHFE:上海期货交易所
24 | * DCE:大连商品交易所
25 | * CZCE:郑州商品交易所
26 | * INE:上海国际能源交易中心
27 | * GFEX:广州期货交易所
28 |
29 |
30 | ## 安装
31 |
32 | 安装环境推荐基于4.0.0版本以上的【[**VeighNa Studio**](https://www.vnpy.com/)】。
33 |
34 | 直接使用pip命令:
35 |
36 | ```
37 | pip install vnpy_xt
38 | ```
39 |
40 |
41 | 或者下载解压后在cmd中运行:
42 |
43 | ```
44 | pip install .
45 | ```
46 |
47 | ## 使用
48 |
49 | 迅投数据试用账号申请链接:[VeighNa社区专属14天试用权限](https://xuntou.net/#/signup?utm_source=vnpy)
50 |
51 | **Token连接**
52 |
53 | 1. 连接前请先确保xtquant模块可以正常加载(在[投研知识库](http://docs.thinktrader.net/)下载xtquant的安装包,解压后放置xtquant包到自己使用的Python环境的site_packages文件夹下)。
54 | 2. 登录[迅投研服务平台](https://xuntou.net/#/userInfo),在【用户中心】-【个人设置】-【接口TOKEN】处获取Token。
55 | 3. 在VeighNa Trader的【全局配置】处进行数据服务配置:
56 | * datafeed.name:xt
57 | * datafeed.username:token
58 | * datafeed.password:填复制的Token
59 |
60 | **客户端连接**
61 |
62 | 1. 连接请先登录迅投极速交易终端,同时确保xtquant模块可以正常加载(点击【下载Python库】-【Python库下载】,下载完成后拷贝“Python库路径”下Lib\site-packages文件夹中的xtquant包到自己使用的Python环境的site_packages文件夹下)。
63 | 2. 在Veighna Trader的【全局配置】处进行数据服务配置:
64 | * datafeed.name:xt
65 | * datafeed.username:client
66 | * datafeed.password:留空
67 | 3. 请注意以客户端方式连接时,需要保持迅投客户端的运行。
68 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "vnpy_xt"
3 | dynamic = ["version"]
4 | description = "RQData datafeed for VeighNa quant trading framework."
5 | readme = "README.md"
6 | license = {text = "MIT"}
7 | authors = [{name = "Xiaoyou Chen", email = "xiaoyou.chen@mail.vnpy.com"}]
8 | classifiers = [
9 | "Development Status :: 5 - Production/Stable",
10 | "License :: OSI Approved :: MIT License",
11 | "Operating System :: Microsoft :: Windows",
12 | "Operating System :: POSIX :: Linux",
13 | "Programming Language :: Python :: 3",
14 | "Programming Language :: Python :: 3.10",
15 | "Programming Language :: Python :: 3.11",
16 | "Programming Language :: Python :: 3.12",
17 | "Programming Language :: Python :: 3.13",
18 | "Topic :: Office/Business :: Financial :: Investment",
19 | "Programming Language :: Python :: Implementation :: CPython",
20 | "Natural Language :: Chinese (Simplified)",
21 | "Typing :: Typed",
22 | ]
23 | requires-python = ">=3.10"
24 | dependencies = [
25 | "xtquant>=240920.1.2",
26 | "filelock>=3.8.2"
27 | ]
28 | keywords = ["quant", "quantitative", "investment", "trading", "algotrading"]
29 |
30 | [project.urls]
31 | "Homepage" = "https://www.vnpy.com"
32 | "Documentation" = "https://www.vnpy.com/docs"
33 | "Changes" = "https://github.com/vnpy/vnpy_xt/blob/master/CHANGELOG.md"
34 | "Source" = "https://github.com/vnpy/vnpy_xt/"
35 | "Forum" = "https://www.vnpy.com/forum"
36 |
37 | [build-system]
38 | requires = ["hatchling>=1.27.0"]
39 | build-backend = "hatchling.build"
40 |
41 | [tool.hatch.version]
42 | path = "vnpy_xt/__init__.py"
43 | pattern = "__version__ = ['\"](?P[^'\"]+)['\"]"
44 |
45 | [tool.hatch.build.targets.wheel]
46 | packages = ["vnpy_xt"]
47 | include-package-data = true
48 |
49 | [tool.hatch.build.targets.sdist]
50 | include = ["vnpy_xt*"]
51 |
52 | [tool.ruff]
53 | target-version = "py310"
54 | output-format = "full"
55 |
56 | [tool.ruff.lint]
57 | select = [
58 | "B", # flake8-bugbear
59 | "E", # pycodestyle error
60 | "F", # pyflakes
61 | "UP", # pyupgrade
62 | "W", # pycodestyle warning
63 | ]
64 | ignore = ["E501"]
65 |
66 | [tool.mypy]
67 | python_version = "3.10"
68 | warn_return_any = true
69 | warn_unused_configs = true
70 | disallow_untyped_defs = true
71 | disallow_incomplete_defs = true
72 | check_untyped_defs = true
73 | disallow_untyped_decorators = true
74 | no_implicit_optional = true
75 | strict_optional = true
76 | warn_redundant_casts = true
77 | warn_unused_ignores = true
78 | warn_no_return = true
79 | ignore_missing_imports = true
80 |
81 | [[tool.mypy.overrides]]
82 | module = ["xtquant.*"]
83 | ignore_missing_imports = true
84 |
--------------------------------------------------------------------------------
/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_xt import XtGateway
6 | from vnpy_datamanager import DataManagerApp
7 |
8 |
9 | # 配置datafeed相关信息,也可以通过vt_setting.json全局文件配置
10 | # from vnpy.trader.setting import SETTINGS
11 | # SETTINGS["datafeed.name"] = "xt"
12 | # SETTINGS["datafeed.username"] = "token"
13 | # SETTINGS["datafeed.password"] = "xxx"
14 |
15 |
16 | def main():
17 | """主入口函数"""
18 | qapp = create_qapp()
19 |
20 | event_engine = EventEngine()
21 | main_engine = MainEngine(event_engine)
22 | main_engine.add_gateway(XtGateway)
23 | main_engine.add_app(DataManagerApp)
24 |
25 | main_window = MainWindow(main_engine, event_engine)
26 | main_window.showMaximized()
27 |
28 | qapp.exec()
29 |
30 |
31 | if __name__ == "__main__":
32 | main()
33 |
--------------------------------------------------------------------------------
/script/xtdata_demo.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "id": "1fee51d2-8d41-468e-b416-a58bed1ddbb9",
6 | "metadata": {},
7 | "source": [
8 | "数据字典地址:http://docs.thinktrader.net/pages/7c0936/"
9 | ]
10 | },
11 | {
12 | "cell_type": "markdown",
13 | "id": "ae164069-0177-4c66-b273-82b21159aae0",
14 | "metadata": {},
15 | "source": [
16 | "若通过客户端下载报错timeout,可在客户端右下角点击【行情】,打开行情源窗口,点击【调度任务】标签页,查看窗口下方“下载信息”处是否有下载客户端下载数据显示。若客户端有正在下载中的任务,可等待客户端下载任务完成后再尝试使用脚本中的函数下载,或中止客户端下载任务后再运行脚本中的下载函数。"
17 | ]
18 | },
19 | {
20 | "cell_type": "code",
21 | "execution_count": null,
22 | "id": "e63c7fc2-2500-4d50-a5af-8af4aaa91c1b",
23 | "metadata": {},
24 | "outputs": [],
25 | "source": [
26 | "# Token连接(若通过客户端连接则无需运行此单元格)\n",
27 | "from vnpy.trader.utility import TEMP_DIR\n",
28 | "\n",
29 | "from xtquant import xtdatacenter as xtdc\n",
30 | "xtdc.set_token(\"xxx\") # 换成自己的Token\n",
31 | "xtdc.set_data_home_dir(str(TEMP_DIR) + \"\\\\xt\")\n",
32 | "xtdc.init()"
33 | ]
34 | },
35 | {
36 | "cell_type": "code",
37 | "execution_count": null,
38 | "id": "cc8d3d17-28bb-42a9-aea7-95879d9a5994",
39 | "metadata": {},
40 | "outputs": [],
41 | "source": [
42 | "from xtquant.xtdata import (\n",
43 | " get_full_tick,\n",
44 | " get_instrument_detail,\n",
45 | " get_local_data,\n",
46 | " download_history_data,\n",
47 | " get_stock_list_in_sector,\n",
48 | " download_financial_data,\n",
49 | " get_financial_data,\n",
50 | " get_market_data_ex\n",
51 | ")"
52 | ]
53 | },
54 | {
55 | "cell_type": "code",
56 | "execution_count": null,
57 | "id": "6c9ca23f-6978-419d-b679-d077694e0ac6",
58 | "metadata": {
59 | "collapsed": true,
60 | "jupyter": {
61 | "outputs_hidden": true
62 | },
63 | "tags": []
64 | },
65 | "outputs": [],
66 | "source": [
67 | "get_full_tick([\"SZO\"])"
68 | ]
69 | },
70 | {
71 | "cell_type": "code",
72 | "execution_count": null,
73 | "id": "50d09893",
74 | "metadata": {},
75 | "outputs": [],
76 | "source": [
77 | "download_history_data(\"\", \"historycontract\")"
78 | ]
79 | },
80 | {
81 | "cell_type": "code",
82 | "execution_count": null,
83 | "id": "4aea61d2-aab4-4189-b19f-862ec22d6a34",
84 | "metadata": {
85 | "collapsed": true,
86 | "jupyter": {
87 | "outputs_hidden": true
88 | },
89 | "tags": []
90 | },
91 | "outputs": [],
92 | "source": [
93 | "# 期权过期合约查询\n",
94 | "contract_data = get_stock_list_in_sector(\"过期中金所\")\n",
95 | "# get_stock_list_in_sector(\"过期上证期权\")\n",
96 | "# get_stock_list_in_sector(\"过期深证期权\")\n",
97 | "print(len(contract_data))"
98 | ]
99 | },
100 | {
101 | "cell_type": "code",
102 | "execution_count": null,
103 | "id": "1f76b380-9d30-4dde-a2b6-913193b49b60",
104 | "metadata": {
105 | "collapsed": true,
106 | "jupyter": {
107 | "outputs_hidden": true
108 | },
109 | "tags": []
110 | },
111 | "outputs": [],
112 | "source": [
113 | "# 获取期权合约信息\n",
114 | "get_instrument_detail(\"90000001.SZO\", True)"
115 | ]
116 | },
117 | {
118 | "cell_type": "code",
119 | "execution_count": null,
120 | "id": "7a04439d-b14f-48a9-9269-420a0c7c2c58",
121 | "metadata": {
122 | "collapsed": true,
123 | "jupyter": {
124 | "outputs_hidden": true
125 | },
126 | "tags": []
127 | },
128 | "outputs": [],
129 | "source": [
130 | "# 期权数据(1d也支持)\n",
131 | "download_history_data(\"90000001.SZO\", \"1m\", \"20200101\", \"20200112\")\n",
132 | "data1 = get_local_data([], [\"90000001.SZO\"], \"1m\", \"20200101\", \"20200112\")\n",
133 | "data1"
134 | ]
135 | },
136 | {
137 | "cell_type": "code",
138 | "execution_count": null,
139 | "id": "32f8a0db",
140 | "metadata": {},
141 | "outputs": [],
142 | "source": [
143 | "# 期货数据(1d也支持)\n",
144 | "download_history_data(\"rb2401.SF\", \"1m\", \"20230601\", \"20231030\")\n",
145 | "data1 = get_local_data([], [\"rb2401.SF\"], \"1m\", \"20231026\", \"20231030\")\n",
146 | "data1"
147 | ]
148 | },
149 | {
150 | "cell_type": "code",
151 | "execution_count": null,
152 | "id": "0b900ae1",
153 | "metadata": {},
154 | "outputs": [],
155 | "source": [
156 | "df = data1[\"90000001.SZO\"]\n",
157 | "df"
158 | ]
159 | },
160 | {
161 | "cell_type": "code",
162 | "execution_count": null,
163 | "id": "1ade0d1d-1905-4316-b1b4-ebd62cc3834c",
164 | "metadata": {
165 | "collapsed": true,
166 | "jupyter": {
167 | "outputs_hidden": true
168 | },
169 | "tags": []
170 | },
171 | "outputs": [],
172 | "source": [
173 | "# 分笔成交数据\n",
174 | "download_history_data(\"rb2309.SF\", \"tick\", \"20230821\", \"20230822\")\n",
175 | "data2 = get_local_data([], [\"rb2309.SF\"], \"tick\", \"20230821\", \"20230822\")\n",
176 | "data2"
177 | ]
178 | },
179 | {
180 | "cell_type": "code",
181 | "execution_count": null,
182 | "id": "7269450a-ea0c-4652-9d0d-bd31b572067a",
183 | "metadata": {
184 | "collapsed": true,
185 | "jupyter": {
186 | "outputs_hidden": true
187 | },
188 | "tags": []
189 | },
190 | "outputs": [],
191 | "source": [
192 | "# 财务数据\n",
193 | "download_financial_data([\"600519.SH\"])\n",
194 | "data3 = get_financial_data([\"600519.SH\"])\n",
195 | "data3"
196 | ]
197 | },
198 | {
199 | "cell_type": "code",
200 | "execution_count": null,
201 | "id": "2dbe8942-b091-4838-b205-67980b45a6ce",
202 | "metadata": {
203 | "collapsed": true,
204 | "jupyter": {
205 | "outputs_hidden": true
206 | },
207 | "tags": []
208 | },
209 | "outputs": [],
210 | "source": [
211 | "# 板块成分股列表\n",
212 | "get_stock_list_in_sector(\"SZO\")"
213 | ]
214 | },
215 | {
216 | "cell_type": "code",
217 | "execution_count": null,
218 | "id": "5cc3e575-aa47-41d9-ab8d-b1d757f9ba52",
219 | "metadata": {
220 | "collapsed": true,
221 | "jupyter": {
222 | "outputs_hidden": true
223 | },
224 | "tags": []
225 | },
226 | "outputs": [],
227 | "source": [
228 | "# 仓单\n",
229 | "download_history_data(\"\", \"warehousereceipt\")\n",
230 | "data4 = get_market_data_ex([], [\"rb.SF\"], period=\"warehousereceipt\")\n",
231 | "data4"
232 | ]
233 | },
234 | {
235 | "cell_type": "code",
236 | "execution_count": null,
237 | "id": "9ff13cb1-2793-42e7-9f2f-10f1175d552b",
238 | "metadata": {
239 | "collapsed": true,
240 | "jupyter": {
241 | "outputs_hidden": true
242 | },
243 | "tags": []
244 | },
245 | "outputs": [],
246 | "source": [
247 | "# 期货席位\n",
248 | "download_history_data(\"rb2401.SF\", \"futureholderrank\", \"20231011\", \"20231012\")\n",
249 | "data5 = get_market_data_ex([], [\"rb2401.SF\"], \"futureholderrank\")[\"rb2401.SF\"]\n",
250 | "data5"
251 | ]
252 | },
253 | {
254 | "cell_type": "code",
255 | "execution_count": null,
256 | "id": "aba5fac4-527f-4688-a1b8-dd68dbd66484",
257 | "metadata": {
258 | "collapsed": true,
259 | "jupyter": {
260 | "outputs_hidden": true
261 | },
262 | "tags": []
263 | },
264 | "outputs": [],
265 | "source": [
266 | "# 资金流向数据(目前只有股票数据)\n",
267 | "# transactioncount1m也支持\n",
268 | "download_history_data(\"000333.SZ\", \"transactioncount1d\")\n",
269 | "data6 = get_market_data_ex([], [\"000333.SZ\"], period=\"transactioncount1d\")\n",
270 | "data6"
271 | ]
272 | },
273 | {
274 | "cell_type": "code",
275 | "execution_count": null,
276 | "id": "51ec686f-d93f-42a6-9f97-bca9c7c75bdc",
277 | "metadata": {
278 | "collapsed": true,
279 | "jupyter": {
280 | "outputs_hidden": true
281 | },
282 | "tags": []
283 | },
284 | "outputs": [],
285 | "source": [
286 | "# 港股资金流向\n",
287 | "# northfinancechange1d也支持\n",
288 | "download_history_data(\"FFFFFF.SGT\", \"northfinancechange1d\")\n",
289 | "data7 = get_market_data_ex([], [\"FFFFFF.SGT\"], period=\"northfinancechange1d\")\n",
290 | "data7"
291 | ]
292 | },
293 | {
294 | "cell_type": "code",
295 | "execution_count": null,
296 | "id": "191e22b6-77c8-4976-a0dd-ea6f0337edf2",
297 | "metadata": {},
298 | "outputs": [],
299 | "source": []
300 | }
301 | ],
302 | "metadata": {
303 | "kernelspec": {
304 | "display_name": "Python 3 (ipykernel)",
305 | "language": "python",
306 | "name": "python3"
307 | },
308 | "language_info": {
309 | "codemirror_mode": {
310 | "name": "ipython",
311 | "version": 3
312 | },
313 | "file_extension": ".py",
314 | "mimetype": "text/x-python",
315 | "name": "python",
316 | "nbconvert_exporter": "python",
317 | "pygments_lexer": "ipython3",
318 | "version": "3.10.4"
319 | }
320 | },
321 | "nbformat": 4,
322 | "nbformat_minor": 5
323 | }
324 |
--------------------------------------------------------------------------------
/vnpy_xt/__init__.py:
--------------------------------------------------------------------------------
1 | # The MIT License (MIT)
2 | #
3 | # Copyright (c) 2015-present, Xiaoyou Chen
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 |
24 | from .xt_datafeed import XtDatafeed as Datafeed
25 | from .xt_gateway import XtGateway
26 |
27 |
28 | __all__ = ["Datafeed", "XtGateway"]
29 |
30 |
31 | __version__ = "1.4.4"
32 |
--------------------------------------------------------------------------------
/vnpy_xt/xt_config.py:
--------------------------------------------------------------------------------
1 | VIP_ADDRESS_LIST = [
2 | "115.231.218.73:55310",
3 | "115.231.218.79:55310"
4 | ]
5 |
6 | LISTEN_PORT = 58620
7 |
--------------------------------------------------------------------------------
/vnpy_xt/xt_datafeed.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime, timedelta, time
2 | from collections.abc import Callable
3 |
4 | from pandas import DataFrame
5 | from xtquant import (
6 | xtdata,
7 | xtdatacenter as xtdc
8 | )
9 | from filelock import FileLock, Timeout
10 |
11 | from vnpy.trader.setting import SETTINGS
12 | from vnpy.trader.constant import Exchange, Interval
13 | from vnpy.trader.object import BarData, TickData, HistoryRequest
14 | from vnpy.trader.utility import ZoneInfo, get_file_path
15 | from vnpy.trader.datafeed import BaseDatafeed
16 |
17 | from .xt_config import VIP_ADDRESS_LIST, LISTEN_PORT
18 |
19 |
20 | INTERVAL_VT2XT: dict[Interval, str] = {
21 | Interval.MINUTE: "1m",
22 | Interval.DAILY: "1d",
23 | Interval.TICK: "tick"
24 | }
25 |
26 | INTERVAL_ADJUSTMENT_MAP: dict[Interval, timedelta] = {
27 | Interval.MINUTE: timedelta(minutes=1),
28 | Interval.DAILY: timedelta() # 日线无需进行调整
29 | }
30 |
31 | EXCHANGE_VT2XT: dict[Exchange, str] = {
32 | Exchange.SSE: "SH",
33 | Exchange.SZSE: "SZ",
34 | Exchange.BSE: "BJ",
35 | Exchange.SHFE: "SF",
36 | Exchange.CFFEX: "IF",
37 | Exchange.INE: "INE",
38 | Exchange.DCE: "DF",
39 | Exchange.CZCE: "ZF",
40 | Exchange.GFEX: "GF",
41 | }
42 |
43 | CHINA_TZ = ZoneInfo("Asia/Shanghai")
44 |
45 |
46 | class XtDatafeed(BaseDatafeed):
47 | """迅投研数据服务接口"""
48 |
49 | lock_filename = "xt_lock"
50 | lock_filepath = get_file_path(lock_filename)
51 |
52 | def __init__(self) -> None:
53 | """"""
54 | self.username: str = SETTINGS["datafeed.username"]
55 | self.password: str = SETTINGS["datafeed.password"]
56 | self.inited: bool = False
57 |
58 | self.lock: FileLock | None = None
59 |
60 | xtdata.enable_hello = False
61 |
62 | def init(self, output: Callable = print) -> bool:
63 | """初始化"""
64 | if self.inited:
65 | return True
66 |
67 | try:
68 | # 使用Token连接,无需启动客户端
69 | if self.username != "client":
70 | self.init_xtdc()
71 |
72 | # 尝试查询合约信息,确认连接成功
73 | xtdata.get_instrument_detail("000001.SZ")
74 | except Exception as ex:
75 | output(f"迅投研数据服务初始化失败,发生异常:{ex}")
76 | return False
77 |
78 | self.inited = True
79 | return True
80 |
81 | def get_lock(self) -> bool:
82 | """获取文件锁,确保单例运行"""
83 | self.lock = FileLock(self.lock_filepath)
84 |
85 | try:
86 | self.lock.acquire(timeout=1)
87 | return True
88 | except Timeout:
89 | return False
90 |
91 | def init_xtdc(self) -> None:
92 | """初始化xtdc服务进程"""
93 | if not self.get_lock():
94 | return
95 |
96 | # 设置token
97 | xtdc.set_token(self.password)
98 |
99 | # 设置连接池
100 | xtdc.set_allow_optmize_address(VIP_ADDRESS_LIST)
101 |
102 | # 开启使用期货真实夜盘时间
103 | xtdc.set_future_realtime_mode(True)
104 |
105 | # 执行初始化,但不启动默认58609端口监听
106 | xtdc.init(False)
107 |
108 | # 设置监听端口
109 | xtdc.listen(port=LISTEN_PORT)
110 |
111 | def query_bar_history(self, req: HistoryRequest, output: Callable = print) -> list[BarData] | None:
112 | """查询K线数据"""
113 | history: list[BarData] = []
114 |
115 | if not self.inited:
116 | n: bool = self.init(output)
117 | if not n:
118 | return history
119 |
120 | df: DataFrame = get_history_df(req, output)
121 | if df.empty:
122 | return history
123 |
124 | adjustment: timedelta = INTERVAL_ADJUSTMENT_MAP[req.interval]
125 |
126 | # 遍历解析
127 | auction_bar: BarData = None
128 |
129 | for tp in df.itertuples():
130 | # 将迅投研时间戳(K线结束时点)转换为VeighNa时间戳(K线开始时点)
131 | dt: datetime = datetime.fromtimestamp(tp.time / 1000)
132 | dt = dt.replace(tzinfo=CHINA_TZ)
133 | dt = dt - adjustment
134 |
135 | # 日线,过滤尚未走完的当日数据
136 | if req.interval == Interval.DAILY:
137 | incomplete_bar: bool = (
138 | dt.date() == datetime.now().date()
139 | and datetime.now().time() < time(hour=15)
140 | )
141 | if incomplete_bar:
142 | continue
143 | # 分钟线,过滤盘前集合竞价数据(合并到开盘后第1根K线中)
144 | else:
145 | if (
146 | req.exchange in (Exchange.SSE, Exchange.SZSE, Exchange.BSE, Exchange.CFFEX)
147 | and dt.time() == time(hour=9, minute=29)
148 | ) or (
149 | req.exchange in (Exchange.SHFE, Exchange.INE, Exchange.DCE, Exchange.CZCE, Exchange.GFEX)
150 | and dt.time() in (time(hour=8, minute=59), time(hour=20, minute=59))
151 | ):
152 | auction_bar = BarData(
153 | symbol=req.symbol,
154 | exchange=req.exchange,
155 | datetime=dt,
156 | open_price=float(tp.open),
157 | volume=float(tp.volume),
158 | turnover=float(tp.amount),
159 | gateway_name="XT"
160 | )
161 | continue
162 |
163 | # 生成K线对象
164 | bar: BarData = BarData(
165 | symbol=req.symbol,
166 | exchange=req.exchange,
167 | datetime=dt,
168 | interval=req.interval,
169 | volume=float(tp.volume),
170 | turnover=float(tp.amount),
171 | open_interest=float(tp.openInterest),
172 | open_price=float(tp.open),
173 | high_price=float(tp.high),
174 | low_price=float(tp.low),
175 | close_price=float(tp.close),
176 | gateway_name="XT"
177 | )
178 |
179 | # 合并集合竞价数据
180 | if auction_bar and auction_bar.volume:
181 | bar.open_price = auction_bar.open_price
182 | bar.high_price = max(bar.high_price, auction_bar.open_price)
183 | bar.low_price = min(bar.low_price, auction_bar.open_price)
184 | bar.volume += auction_bar.volume
185 | bar.turnover += auction_bar.turnover
186 | auction_bar = None
187 |
188 | history.append(bar)
189 |
190 | return history
191 |
192 | def query_tick_history(self, req: HistoryRequest, output: Callable = print) -> list[TickData] | None:
193 | """查询Tick数据"""
194 | history: list[TickData] = []
195 |
196 | if not self.inited:
197 | n: bool = self.init(output)
198 | if not n:
199 | return history
200 |
201 | df: DataFrame = get_history_df(req, output)
202 | if df.empty:
203 | return history
204 |
205 | # 遍历解析
206 | for tp in df.itertuples():
207 | dt: datetime = datetime.fromtimestamp(tp.time / 1000)
208 | dt = dt.replace(tzinfo=CHINA_TZ)
209 |
210 | bidPrice: list[float] = tp.bidPrice
211 | askPrice: list[float] = tp.askPrice
212 | bidVol: list[float] = tp.bidVol
213 | askVol: list[float] = tp.askVol
214 |
215 | tick: TickData = TickData(
216 | symbol=req.symbol,
217 | exchange=req.exchange,
218 | datetime=dt,
219 | volume=float(tp.volume),
220 | turnover=float(tp.amount),
221 | open_interest=float(tp.openInt),
222 | open_price=float(tp.open),
223 | high_price=float(tp.high),
224 | low_price=float(tp.low),
225 | last_price=float(tp.lastPrice),
226 | pre_close=float(tp.lastClose),
227 | bid_price_1=float(bidPrice[0]),
228 | ask_price_1=float(askPrice[0]),
229 | bid_volume_1=float(bidVol[0]),
230 | ask_volume_1=float(askVol[0]),
231 | gateway_name="XT",
232 | )
233 |
234 | bid_price_2: float = float(bidPrice[1])
235 | if bid_price_2:
236 | tick.bid_price_2 = bid_price_2
237 | tick.bid_price_3 = float(bidPrice[2])
238 | tick.bid_price_4 = float(bidPrice[3])
239 | tick.bid_price_5 = float(bidPrice[4])
240 |
241 | tick.ask_price_2 = float(askPrice[1])
242 | tick.ask_price_3 = float(askPrice[2])
243 | tick.ask_price_4 = float(askPrice[3])
244 | tick.ask_price_5 = float(askPrice[4])
245 |
246 | tick.bid_volume_2 = float(bidVol[1])
247 | tick.bid_volume_3 = float(bidVol[2])
248 | tick.bid_volume_4 = float(bidVol[3])
249 | tick.bid_volume_5 = float(bidVol[4])
250 |
251 | tick.ask_volume_2 = float(askVol[1])
252 | tick.ask_volume_3 = float(askVol[2])
253 | tick.ask_volume_4 = float(askVol[3])
254 | tick.ask_volume_5 = float(askVol[4])
255 |
256 | history.append(tick)
257 |
258 | return history
259 |
260 |
261 | def get_history_df(req: HistoryRequest, output: Callable = print) -> DataFrame:
262 | """获取历史数据DataFrame"""
263 | symbol: str = req.symbol
264 | exchange: Exchange = req.exchange
265 | start_dt: datetime = req.start
266 | end_dt: datetime = req.end
267 | interval: Interval = req.interval
268 |
269 | if not interval:
270 | interval = Interval.TICK
271 |
272 | xt_interval: str | None = INTERVAL_VT2XT.get(interval, None)
273 | if not xt_interval:
274 | output(f"迅投研查询历史数据失败:不支持的时间周期{interval.value}")
275 | return DataFrame()
276 |
277 | # 为了查询夜盘数据
278 | end_dt += timedelta(1)
279 |
280 | # 从服务器下载获取
281 | xt_symbol: str = symbol + "." + EXCHANGE_VT2XT[exchange]
282 | start: str = start_dt.strftime("%Y%m%d%H%M%S")
283 | end: str = end_dt.strftime("%Y%m%d%H%M%S")
284 |
285 | if exchange in (Exchange.SSE, Exchange.SZSE) and len(symbol) > 6:
286 | xt_symbol += "O"
287 |
288 | xtdata.download_history_data(xt_symbol, xt_interval, start, end)
289 | data: dict = xtdata.get_local_data([], [xt_symbol], xt_interval, start, end, -1, "front_ratio", False) # 默认等比前复权
290 |
291 | df: DataFrame = data[xt_symbol]
292 | return df
293 |
--------------------------------------------------------------------------------
/vnpy_xt/xt_gateway.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from collections.abc import Callable
3 | from threading import Thread
4 | from typing import Any
5 |
6 | from xtquant import (
7 | xtdata,
8 | xtdatacenter as xtdc
9 | )
10 | from xtquant import xtconstant
11 | from xtquant.xttrader import XtQuantTrader, XtQuantTraderCallback
12 | from xtquant.xttype import (
13 | StockAccount,
14 | XtAsset,
15 | XtOrder,
16 | XtPosition,
17 | XtTrade,
18 | XtOrderResponse,
19 | XtCancelOrderResponse,
20 | XtOrderError,
21 | XtCancelError
22 | )
23 | from filelock import FileLock, Timeout
24 |
25 | from vnpy.event import EventEngine, EVENT_TIMER, Event
26 | from vnpy.trader.gateway import BaseGateway
27 | from vnpy.trader.object import (
28 | OrderRequest,
29 | CancelRequest,
30 | SubscribeRequest,
31 | ContractData,
32 | TickData,
33 | HistoryRequest,
34 | OptionType,
35 | OrderData,
36 | Status,
37 | Direction,
38 | OrderType,
39 | AccountData,
40 | PositionData,
41 | TradeData,
42 | Offset
43 | )
44 | from vnpy.trader.constant import (
45 | Exchange,
46 | Product
47 | )
48 | from vnpy.trader.utility import (
49 | ZoneInfo,
50 | get_file_path,
51 | round_to
52 | )
53 |
54 | from .xt_config import VIP_ADDRESS_LIST, LISTEN_PORT
55 |
56 |
57 | # 交易所映射
58 | EXCHANGE_VT2XT: dict[Exchange, str] = {
59 | Exchange.SSE: "SH",
60 | Exchange.SZSE: "SZ",
61 | Exchange.BSE: "BJ",
62 | Exchange.SHFE: "SF",
63 | Exchange.CFFEX: "IF",
64 | Exchange.INE: "INE",
65 | Exchange.DCE: "DF",
66 | Exchange.CZCE: "ZF",
67 | Exchange.GFEX: "GF",
68 | }
69 |
70 | EXCHANGE_XT2VT: dict[str, Exchange] = {v: k for k, v in EXCHANGE_VT2XT.items()}
71 | EXCHANGE_XT2VT["SHO"] = Exchange.SSE
72 | EXCHANGE_XT2VT["SZO"] = Exchange.SZSE
73 |
74 |
75 | # 委托状态映射
76 | STATUS_XT2VT: dict[str, Status] = {
77 | xtconstant.ORDER_UNREPORTED: Status.SUBMITTING,
78 | xtconstant.ORDER_WAIT_REPORTING: Status.SUBMITTING,
79 | xtconstant.ORDER_REPORTED: Status.NOTTRADED,
80 | xtconstant.ORDER_REPORTED_CANCEL: Status.CANCELLED,
81 | xtconstant.ORDER_PARTSUCC_CANCEL: Status.CANCELLED,
82 | xtconstant.ORDER_PART_CANCEL: Status.CANCELLED,
83 | xtconstant.ORDER_CANCELED: Status.CANCELLED,
84 | xtconstant.ORDER_PART_SUCC: Status.PARTTRADED,
85 | xtconstant.ORDER_SUCCEEDED: Status.ALLTRADED,
86 | xtconstant.ORDER_JUNK: Status.REJECTED
87 | }
88 |
89 | # 多空方向映射
90 | DIRECTION_VT2XT: dict[tuple, str] = {
91 | (Direction.LONG, Offset.NONE): xtconstant.STOCK_BUY,
92 | (Direction.SHORT, Offset.NONE): xtconstant.STOCK_SELL,
93 | (Direction.LONG, Offset.OPEN): xtconstant.STOCK_OPTION_BUY_OPEN,
94 | (Direction.LONG, Offset.CLOSE): xtconstant.STOCK_OPTION_BUY_CLOSE,
95 | (Direction.SHORT, Offset.OPEN): xtconstant.STOCK_OPTION_SELL_OPEN,
96 | (Direction.SHORT, Offset.CLOSE): xtconstant.STOCK_OPTION_SELL_CLOSE,
97 | }
98 | DIRECTION_XT2VT: dict[str, tuple] = {v: k for k, v in DIRECTION_VT2XT.items()}
99 |
100 | POSDIRECTION_XT2VT: dict[int, Direction] = {
101 | xtconstant.DIRECTION_FLAG_BUY: Direction.LONG,
102 | xtconstant.DIRECTION_FLAG_SELL: Direction.SHORT
103 | }
104 |
105 | # 委托类型映射
106 | ORDERTYPE_VT2XT: dict[tuple, int] = {
107 | (Exchange.SSE, OrderType.LIMIT): xtconstant.FIX_PRICE,
108 | (Exchange.SZSE, OrderType.LIMIT): xtconstant.FIX_PRICE,
109 | (Exchange.BSE, OrderType.LIMIT): xtconstant.FIX_PRICE,
110 | }
111 | ORDERTYPE_XT2VT: dict[int, OrderType] = {
112 | 50: OrderType.LIMIT,
113 | }
114 |
115 | # 其他常量
116 | CHINA_TZ = ZoneInfo("Asia/Shanghai") # 中国时区
117 |
118 |
119 | # 全局缓存字典
120 | symbol_contract_map: dict[str, ContractData] = {} # 合约数据
121 | symbol_limit_map: dict[str, tuple[float, float]] = {} # 涨跌停价
122 |
123 |
124 | class XtGateway(BaseGateway):
125 | """
126 | VeighNa用于对接迅投研的实时行情接口。
127 | """
128 |
129 | default_name: str = "XT"
130 |
131 | default_setting: dict[str, Any] = {
132 | "token": "",
133 | "股票市场": ["是", "否"],
134 | "期货市场": ["是", "否"],
135 | "期权市场": ["是", "否"],
136 | "仿真交易": ["是", "否"],
137 | "账号类型": ["股票", "股票期权"],
138 | "QMT路径": "",
139 | "资金账号": ""
140 | }
141 |
142 | exchanges: list[str] = list(EXCHANGE_VT2XT.keys())
143 |
144 | def __init__(self, event_engine: EventEngine, gateway_name: str) -> None:
145 | """构造函数"""
146 | super().__init__(event_engine, gateway_name)
147 |
148 | self.md_api: XtMdApi = XtMdApi(self)
149 | self.td_api: XtTdApi = XtTdApi(self)
150 |
151 | self.trading: bool = False
152 | self.orders: dict[str, OrderData] = {}
153 | self.count: int = 0
154 |
155 | self.thread: Thread | None = None
156 |
157 | def connect(self, setting: dict) -> None:
158 | """连接交易接口"""
159 | if self.thread:
160 | return
161 |
162 | self.thread = Thread(target=self._connect, args=(setting,))
163 | self.thread.start()
164 |
165 | def _connect(self, setting: dict) -> None:
166 | """连接交易接口"""
167 | token: str = setting["token"]
168 |
169 | stock_active: bool = setting["股票市场"] == "是"
170 | futures_active: bool = setting["期货市场"] == "是"
171 | option_active: bool = setting["期权市场"] == "是"
172 |
173 | self.md_api.connect(token, stock_active, futures_active, option_active)
174 |
175 | self.trading = setting["仿真交易"] == "是"
176 | if self.trading:
177 | path: str = setting["QMT路径"] + "\\userdata"
178 |
179 | accountid: str = setting["资金账号"]
180 |
181 | if setting["账号类型"] == "股票":
182 | account_type: str = "STOCK"
183 | else:
184 | account_type = "STOCK_OPTION"
185 |
186 | self.td_api.connect(path, accountid, account_type)
187 | self.init_query()
188 |
189 | def subscribe(self, req: SubscribeRequest) -> None:
190 | """订阅行情"""
191 | self.md_api.subscribe(req)
192 |
193 | def send_order(self, req: OrderRequest) -> str:
194 | """委托下单"""
195 | if self.trading:
196 | return self.td_api.send_order(req)
197 | else:
198 | return ""
199 |
200 | def cancel_order(self, req: CancelRequest) -> None:
201 | """委托撤单"""
202 | if self.trading:
203 | self.td_api.cancel_order(req)
204 |
205 | def query_account(self) -> None:
206 | """查询资金"""
207 | if self.trading:
208 | self.td_api.query_account()
209 |
210 | def query_position(self) -> None:
211 | """查询持仓"""
212 | if self.trading:
213 | self.td_api.query_position()
214 |
215 | def query_history(self, req: HistoryRequest) -> None:
216 | """查询历史数据"""
217 | return None
218 |
219 | def on_order(self, order: OrderData) -> None:
220 | """推送委托数据"""
221 | self.orders[order.orderid] = order
222 | super().on_order(order)
223 |
224 | def get_order(self, orderid: str) -> OrderData:
225 | """查询委托数据"""
226 | return self.orders.get(orderid, None)
227 |
228 | def close(self) -> None:
229 | """关闭接口"""
230 | if self.trading:
231 | self.td_api.close()
232 |
233 | def process_timer_event(self, event: Event) -> None:
234 | """定时事件处理"""
235 | self.count += 1
236 | if self.count < 2:
237 | return
238 | self.count = 0
239 |
240 | func = self.query_functions.pop(0)
241 | func()
242 | self.query_functions.append(func)
243 |
244 | def init_query(self) -> None:
245 | """初始化查询任务"""
246 | self.query_functions: list = [self.query_account, self.query_position]
247 | self.event_engine.register(EVENT_TIMER, self.process_timer_event)
248 |
249 |
250 | class XtMdApi:
251 | """行情API"""
252 |
253 | lock_filename = "xt_lock"
254 | lock_filepath = get_file_path(lock_filename)
255 |
256 | def __init__(self, gateway: XtGateway) -> None:
257 | """构造函数"""
258 | self.gateway: XtGateway = gateway
259 | self.gateway_name: str = gateway.gateway_name
260 |
261 | self.inited: bool = False
262 | self.subscribed: set = set()
263 |
264 | self.token: str = ""
265 | self.stock_active: bool = False
266 | self.futures_active: bool = False
267 | self.option_active: bool = False
268 |
269 | def onMarketData(self, data: dict) -> None:
270 | """行情推送回调"""
271 | for xt_symbol, buf in data.items():
272 | for d in buf:
273 | symbol, xt_exchange = xt_symbol.split(".")
274 | exchange = EXCHANGE_XT2VT[xt_exchange]
275 |
276 | tick: TickData = TickData(
277 | symbol=symbol,
278 | exchange=exchange,
279 | datetime=generate_datetime(d["time"]),
280 | volume=d["volume"],
281 | turnover=d["amount"],
282 | open_interest=d["openInt"],
283 | gateway_name=self.gateway_name
284 | )
285 |
286 | contract = symbol_contract_map[tick.vt_symbol]
287 | tick.name = contract.name
288 |
289 | bp_data: list = d["bidPrice"]
290 | ap_data: list = d["askPrice"]
291 | bv_data: list = d["bidVol"]
292 | av_data: list = d["askVol"]
293 |
294 | tick.bid_price_1 = round_to(bp_data[0], contract.pricetick)
295 | tick.bid_price_2 = round_to(bp_data[1], contract.pricetick)
296 | tick.bid_price_3 = round_to(bp_data[2], contract.pricetick)
297 | tick.bid_price_4 = round_to(bp_data[3], contract.pricetick)
298 | tick.bid_price_5 = round_to(bp_data[4], contract.pricetick)
299 |
300 | tick.ask_price_1 = round_to(ap_data[0], contract.pricetick)
301 | tick.ask_price_2 = round_to(ap_data[1], contract.pricetick)
302 | tick.ask_price_3 = round_to(ap_data[2], contract.pricetick)
303 | tick.ask_price_4 = round_to(ap_data[3], contract.pricetick)
304 | tick.ask_price_5 = round_to(ap_data[4], contract.pricetick)
305 |
306 | tick.bid_volume_1 = bv_data[0]
307 | tick.bid_volume_2 = bv_data[1]
308 | tick.bid_volume_3 = bv_data[2]
309 | tick.bid_volume_4 = bv_data[3]
310 | tick.bid_volume_5 = bv_data[4]
311 |
312 | tick.ask_volume_1 = av_data[0]
313 | tick.ask_volume_2 = av_data[1]
314 | tick.ask_volume_3 = av_data[2]
315 | tick.ask_volume_4 = av_data[3]
316 | tick.ask_volume_5 = av_data[4]
317 |
318 | tick.last_price = round_to(d["lastPrice"], contract.pricetick)
319 | tick.open_price = round_to(d["open"], contract.pricetick)
320 | tick.high_price = round_to(d["high"], contract.pricetick)
321 | tick.low_price = round_to(d["low"], contract.pricetick)
322 | tick.pre_close = round_to(d["lastClose"], contract.pricetick)
323 |
324 | if tick.vt_symbol in symbol_limit_map:
325 | tick.limit_up, tick.limit_down = symbol_limit_map[tick.vt_symbol]
326 |
327 | # 判断收盘状态
328 | tick.extra = {
329 | "raw": d,
330 | "market_closed": False,
331 | }
332 |
333 | # 非衍生品可以通过openInt字段判断证券状态
334 | if contract.product not in {Product.FUTURES, Product.OPTION}:
335 | tick.extra["market_closed"] = d["openInt"] == 15
336 | # 衍生品该字段为持仓量,需要通过结算价判断
337 | elif d["settlementPrice"] > 0:
338 | tick.extra["market_closed"] = True
339 |
340 | self.gateway.on_tick(tick)
341 |
342 | def connect(
343 | self,
344 | token: str,
345 | stock_active: bool,
346 | futures_active: bool,
347 | option_active: bool
348 | ) -> None:
349 | """连接"""
350 | self.gateway.write_log("开始启动行情服务,请稍等")
351 |
352 | self.token = token
353 | self.stock_active = stock_active
354 | self.futures_active = futures_active
355 | self.option_active = option_active
356 |
357 | if self.inited:
358 | self.gateway.write_log("行情接口已经初始化,请勿重复操作")
359 | return
360 |
361 | try:
362 | self.init_xtdc()
363 |
364 | # 尝试查询合约信息,确认连接成功
365 | xtdata.get_instrument_detail("000001.SZ")
366 | except Exception as ex:
367 | self.gateway.write_log(f"迅投研数据服务初始化失败,发生异常:{ex}")
368 | return
369 |
370 | self.inited = True
371 |
372 | self.gateway.write_log("行情接口连接成功")
373 |
374 | self.query_contracts()
375 |
376 | def get_lock(self) -> bool:
377 | """获取文件锁,确保单例运行"""
378 | self.lock = FileLock(self.lock_filepath)
379 |
380 | try:
381 | self.lock.acquire(timeout=1)
382 | return True
383 | except Timeout:
384 | return False
385 |
386 | def init_xtdc(self) -> None:
387 | """初始化xtdc服务进程"""
388 | if not self.get_lock():
389 | return
390 |
391 | # 设置token
392 | xtdc.set_token(self.token)
393 |
394 | # 设置连接池
395 | xtdc.set_allow_optmize_address(VIP_ADDRESS_LIST)
396 |
397 | # 开启使用期货真实夜盘时间
398 | xtdc.set_future_realtime_mode(True)
399 |
400 | # 执行初始化,但不启动默认58609端口监听
401 | xtdc.init(False)
402 |
403 | # 设置监听端口
404 | xtdc.listen(port=LISTEN_PORT)
405 |
406 | def query_contracts(self) -> None:
407 | """查询合约信息"""
408 | if self.stock_active:
409 | self.query_stock_contracts()
410 |
411 | if self.futures_active:
412 | self.query_future_contracts()
413 |
414 | if self.option_active:
415 | self.query_option_contracts()
416 |
417 | self.gateway.write_log("合约信息查询成功")
418 |
419 | def query_stock_contracts(self) -> None:
420 | """查询股票合约信息"""
421 | xt_symbols: list[str] = []
422 | markets: list = [
423 | "沪深A股",
424 | "沪深转债",
425 | "沪深ETF",
426 | "沪深指数",
427 | "京市A股"
428 | ]
429 |
430 | for i in markets:
431 | names: list = xtdata.get_stock_list_in_sector(i)
432 | xt_symbols.extend(names)
433 |
434 | for xt_symbol in xt_symbols:
435 | # 筛选需要的合约
436 | product = None
437 | symbol, xt_exchange = xt_symbol.split(".")
438 |
439 | if xt_exchange == "SZ":
440 | if xt_symbol.startswith("00"):
441 | product = Product.EQUITY
442 | elif xt_symbol.startswith("159"):
443 | product = Product.FUND
444 | else:
445 | product = Product.INDEX
446 | elif xt_exchange == "SH":
447 | if xt_symbol.startswith(("60", "68")):
448 | product = Product.EQUITY
449 | elif xt_symbol.startswith("51"):
450 | product = Product.FUND
451 | else:
452 | product = Product.INDEX
453 | elif xt_exchange == "BJ":
454 | product = Product.EQUITY
455 |
456 | if not product:
457 | continue
458 |
459 | # 生成并推送合约信息
460 | data: dict = xtdata.get_instrument_detail(xt_symbol)
461 | if data is None:
462 | self.gateway.write_log(f"合约{xt_symbol}信息查询失败")
463 | continue
464 |
465 | contract: ContractData = ContractData(
466 | symbol=symbol,
467 | exchange=EXCHANGE_XT2VT[xt_exchange],
468 | name=data["InstrumentName"],
469 | product=product,
470 | size=data["VolumeMultiple"],
471 | pricetick=data["PriceTick"],
472 | history_data=False,
473 | gateway_name=self.gateway_name
474 | )
475 |
476 | symbol_contract_map[contract.vt_symbol] = contract
477 | symbol_limit_map[contract.vt_symbol] = (data["UpStopPrice"], data["DownStopPrice"])
478 |
479 | self.gateway.on_contract(contract)
480 |
481 | def query_future_contracts(self) -> None:
482 | """查询期货合约信息"""
483 | xt_symbols: list[str] = []
484 | markets: list = [
485 | "中金所期货",
486 | "上期所期货",
487 | "能源中心期货",
488 | "大商所期货",
489 | "郑商所期货",
490 | "广期所期货"
491 | ]
492 |
493 | for i in markets:
494 | names: list = xtdata.get_stock_list_in_sector(i)
495 | xt_symbols.extend(names)
496 |
497 | for xt_symbol in xt_symbols:
498 | # 筛选需要的合约
499 | product = None
500 | symbol, xt_exchange = xt_symbol.split(".")
501 |
502 | if xt_exchange == "ZF" and len(symbol) > 6 and "&" not in symbol:
503 | product = Product.OPTION
504 | elif xt_exchange in ("IF", "GF") and "-" in symbol:
505 | product = Product.OPTION
506 | elif xt_exchange in ("DF", "INE", "SF") and ("C" in symbol or "P" in symbol) and "SP" not in symbol:
507 | product = Product.OPTION
508 | else:
509 | product = Product.FUTURES
510 |
511 | # 生成并推送合约信息
512 | if product == Product.OPTION:
513 | data: dict = xtdata.get_instrument_detail(xt_symbol, True)
514 | else:
515 | data = xtdata.get_instrument_detail(xt_symbol)
516 |
517 | if not data["ExpireDate"]:
518 | if "00" not in symbol:
519 | continue
520 |
521 | contract: ContractData = ContractData(
522 | symbol=symbol,
523 | exchange=EXCHANGE_XT2VT[xt_exchange],
524 | name=data["InstrumentName"],
525 | product=product,
526 | size=data["VolumeMultiple"],
527 | pricetick=data["PriceTick"],
528 | history_data=False,
529 | gateway_name=self.gateway_name
530 | )
531 |
532 | symbol_contract_map[contract.vt_symbol] = contract
533 | symbol_limit_map[contract.vt_symbol] = (data["UpStopPrice"], data["DownStopPrice"])
534 |
535 | self.gateway.on_contract(contract)
536 |
537 | def query_option_contracts(self) -> None:
538 | """查询期权合约信息"""
539 | xt_symbols: list[str] = []
540 |
541 | markets: list = [
542 | "上证期权",
543 | "深证期权",
544 | "中金所期权",
545 | "上期所期权",
546 | "能源中心期权",
547 | "大商所期权",
548 | "郑商所期权",
549 | "广期所期权"
550 | ]
551 |
552 | for i in markets:
553 | names: list = xtdata.get_stock_list_in_sector(i)
554 | xt_symbols.extend(names)
555 |
556 | for xt_symbol in xt_symbols:
557 | ""
558 | _, xt_exchange = xt_symbol.split(".")
559 |
560 | if xt_exchange in {"SHO", "SZO"}:
561 | contract = process_etf_option(xtdata.get_instrument_detail, xt_symbol, self.gateway_name)
562 | else:
563 | contract = process_futures_option(xtdata.get_instrument_detail, xt_symbol, self.gateway_name)
564 |
565 | if contract:
566 | symbol_contract_map[contract.vt_symbol] = contract
567 |
568 | self.gateway.on_contract(contract)
569 |
570 | def subscribe(self, req: SubscribeRequest) -> None:
571 | """订阅行情"""
572 | if req.vt_symbol not in symbol_contract_map:
573 | return
574 |
575 | xt_exchange: str = EXCHANGE_VT2XT[req.exchange]
576 | if xt_exchange in {"SH", "SZ"} and len(req.symbol) > 6:
577 | xt_exchange += "O"
578 |
579 | xt_symbol: str = req.symbol + "." + xt_exchange
580 |
581 | if xt_symbol not in self.subscribed:
582 | xtdata.subscribe_quote(stock_code=xt_symbol, period="tick", callback=self.onMarketData)
583 | self.subscribed.add(xt_symbol)
584 |
585 | def close(self) -> None:
586 | """关闭连接"""
587 | pass
588 |
589 |
590 | class XtTdApi(XtQuantTraderCallback):
591 | """交易API"""
592 |
593 | def __init__(self, gateway: XtGateway):
594 | """构造函数"""
595 | super().__init__()
596 |
597 | self.gateway: XtGateway = gateway
598 | self.gateway_name: str = gateway.gateway_name
599 |
600 | self.inited: bool = False
601 | self.connected: bool = False
602 |
603 | self.account_id: str = ""
604 | self.path: str = ""
605 | self.account_type: str = ""
606 |
607 | self.order_count: int = 0
608 |
609 | self.active_localid_sysid_map: dict[str, str] = {}
610 |
611 | self.xt_client: XtQuantTrader = None
612 | self.xt_account: StockAccount = None
613 |
614 | def on_connected(self) -> None:
615 | """
616 | 连接成功推送
617 | """
618 | self.gateway.write_log("交易接口连接成功")
619 |
620 | def on_disconnected(self) -> None:
621 | """连接断开"""
622 | self.gateway.write_log("交易接口连接断开,请检查与客户端的连接状态")
623 | self.connected = False
624 |
625 | # 尝试重连,重连需要更换session_id
626 | session: int = int(float(datetime.now().strftime("%H%M%S.%f")) * 1000)
627 | connect_result: int = self.connect(self.path, self.accountid, self.account_type, session)
628 |
629 | if connect_result:
630 | self.gateway.write_log("交易接口重连失败")
631 | else:
632 | self.gateway.write_log("交易接口重连成功")
633 |
634 | def on_stock_trade(self, xt_trade: XtTrade) -> None:
635 | """成交变动推送"""
636 | if not xt_trade.order_remark:
637 | return
638 |
639 | symbol, xt_exchange = xt_trade.stock_code.split(".")
640 |
641 | direction, offset = DIRECTION_XT2VT.get(xt_trade.order_type, (None, None))
642 | if direction is None:
643 | return
644 |
645 | trade: TradeData = TradeData(
646 | symbol=symbol,
647 | exchange=EXCHANGE_XT2VT[xt_exchange],
648 | orderid=xt_trade.order_remark,
649 | tradeid=xt_trade.traded_id,
650 | direction=direction,
651 | offset=offset,
652 | price=xt_trade.traded_price,
653 | volume=xt_trade.traded_volume,
654 | datetime=generate_datetime(xt_trade.traded_time, False),
655 | gateway_name=self.gateway_name
656 | )
657 |
658 | contract: ContractData = symbol_contract_map.get(trade.vt_symbol, None)
659 | if contract:
660 | trade.price = round_to(trade.price, contract.pricetick)
661 |
662 | self.gateway.on_trade(trade)
663 |
664 | def on_stock_order(self, xt_order: XtOrder) -> None:
665 | """委托回报推送"""
666 | # 过滤非VeighNa Trader发出的委托
667 | if not xt_order.order_remark:
668 | return
669 |
670 | # 过滤不支持的委托类型
671 | type: OrderType = ORDERTYPE_XT2VT.get(xt_order.price_type, None)
672 | if not type:
673 | return
674 |
675 | direction, offset = DIRECTION_XT2VT.get(xt_order.order_type, (None, None))
676 | if direction is None:
677 | return
678 |
679 | symbol, xt_exchange = xt_order.stock_code.split(".")
680 |
681 | order: OrderData = OrderData(
682 | symbol=symbol,
683 | exchange=EXCHANGE_XT2VT[xt_exchange],
684 | orderid=xt_order.order_remark,
685 | direction=direction,
686 | offset=offset,
687 | type=type, # 目前测出来与文档不同,限价返回50,市价返回88
688 | price=xt_order.price,
689 | volume=xt_order.order_volume,
690 | traded=xt_order.traded_volume,
691 | status=STATUS_XT2VT.get(xt_order.order_status, Status.SUBMITTING),
692 | datetime=generate_datetime(xt_order.order_time, False),
693 | gateway_name=self.gateway_name
694 | )
695 |
696 | if order.is_active():
697 | self.active_localid_sysid_map[xt_order.order_remark] = xt_order.order_sysid
698 | else:
699 | self.active_localid_sysid_map.pop(xt_order.order_remark, None)
700 |
701 | contract: ContractData = symbol_contract_map.get(order.vt_symbol, None)
702 | if contract:
703 | order.price = round_to(order.price, contract.pricetick)
704 |
705 | self.gateway.on_order(order)
706 |
707 | def on_query_order_async(self, xt_orders: list[XtOrder]) -> None:
708 | """委托信息异步查询回报"""
709 | if not xt_orders:
710 | return
711 |
712 | for data in xt_orders:
713 | self.on_stock_order(data)
714 |
715 | self.gateway.write_log("委托信息查询成功")
716 |
717 | def on_query_asset_async(self, xt_asset: XtAsset) -> None:
718 | """资金信息异步查询回报"""
719 | if not xt_asset:
720 | return
721 |
722 | account: AccountData = AccountData(
723 | accountid=xt_asset.account_id,
724 | balance=xt_asset.total_asset,
725 | frozen=xt_asset.frozen_cash,
726 | gateway_name=self.gateway_name
727 | )
728 | account.available = xt_asset.cash
729 |
730 | self.gateway.on_account(account)
731 |
732 | def on_query_trades_async(self, xt_trades: list[XtTrade]) -> None:
733 | """成交信息异步查询回报"""
734 | if not xt_trades:
735 | return
736 |
737 | for xt_trade in xt_trades:
738 | self.on_stock_trade(xt_trade)
739 |
740 | self.gateway.write_log("成交信息查询成功")
741 |
742 | def on_query_positions_async(self, xt_positions: list[XtPosition]) -> None:
743 | """持仓信息异步查询回报"""
744 | if not xt_positions:
745 | return
746 |
747 | for xt_position in xt_positions:
748 | if self.account_type == "STOCK":
749 | direction: Direction = Direction.NET
750 | else:
751 | direction = POSDIRECTION_XT2VT.get(xt_position.direction, "")
752 |
753 | if not direction:
754 | continue
755 |
756 | symbol, xt_exchange = xt_position.stock_code.split(".")
757 |
758 | position: PositionData = PositionData(
759 | symbol=symbol,
760 | exchange=EXCHANGE_XT2VT[xt_exchange],
761 | direction=direction,
762 | volume=xt_position.volume,
763 | yd_volume=xt_position.can_use_volume,
764 | frozen=xt_position.volume - xt_position.can_use_volume,
765 | price=xt_position.open_price,
766 | gateway_name=self.gateway_name
767 | )
768 |
769 | self.gateway.on_position(position)
770 |
771 | def on_order_error(self, xt_error: XtOrderError) -> None:
772 | """委托失败推送"""
773 | order: OrderData = self.gateway.get_order(xt_error.order_remark)
774 | if order:
775 | order.status = Status.REJECTED
776 | self.gateway.on_order(order)
777 |
778 | self.gateway.write_log(f"交易委托失败, 错误代码{xt_error.error_id}, 错误信息{xt_error.error_msg}")
779 |
780 | def on_cancel_error(self, xt_error: XtCancelError) -> None:
781 | """撤单失败推送"""
782 | self.gateway.write_log(f"交易撤单失败, 错误代码{xt_error.error_id}, 错误信息{xt_error.error_msg}")
783 |
784 | def on_order_stock_async_response(self, response: XtOrderResponse) -> None:
785 | """异步下单回报推送"""
786 | if response.error_msg:
787 | self.gateway.write_log(f"委托请求提交失败:{response.error_msg},本地委托号{response.order_remark}")
788 | else:
789 | self.gateway.write_log(f"委托请求提交成功,本地委托号{response.order_remark}")
790 |
791 | def on_cancel_order_stock_async_response(self, response: XtCancelOrderResponse) -> None:
792 | """异步撤单回报推送"""
793 | if response.error_msg:
794 | self.gateway.write_log(f"撤单请求提交失败:{response.error_msg},系统委托号{response.order_sysid}")
795 | else:
796 | self.gateway.write_log(f"撤单请求提交成功,系统委托号{response.order_sysid}")
797 |
798 | def connect(self, path: str, accountid: str, account_type: str, session: int = 0) -> int:
799 | """发起连接"""
800 | self.inited = True
801 | self.account_id = accountid
802 | self.path = path
803 | self.account_type = account_type
804 |
805 | # 创建客户端和账号实例
806 | if not session:
807 | session = int(float(datetime.now().strftime("%H%M%S.%f")) * 1000)
808 |
809 | self.xt_client = XtQuantTrader(self.path, session)
810 |
811 | self.xt_account = StockAccount(self.account_id, account_type=self.account_type)
812 |
813 | # 注册回调接口
814 | self.xt_client.register_callback(self)
815 |
816 | # 启动交易线程
817 | self.xt_client.start()
818 |
819 | # 建立交易连接,返回0表示连接成功
820 | connect_result: int = self.xt_client.connect()
821 | if connect_result:
822 | self.gateway.write_log("交易接口连接失败")
823 | return connect_result
824 |
825 | self.connected = True
826 | self.gateway.write_log("交易接口连接成功")
827 |
828 | # 订阅交易回调推送
829 | subscribe_result: int = self.xt_client.subscribe(self.xt_account)
830 | if subscribe_result:
831 | self.gateway.write_log("交易推送订阅失败")
832 | return -1
833 |
834 | self.gateway.write_log("交易推送订阅成功")
835 |
836 | # 初始化数据查询
837 | self.query_account()
838 | self.query_position()
839 | self.query_order()
840 | self.query_trade()
841 |
842 | return connect_result
843 |
844 | def new_orderid(self) -> str:
845 | """生成本地委托号"""
846 | prefix: str = datetime.now().strftime("1%m%d%H%M%S")
847 |
848 | self.order_count += 1
849 | suffix: str = str(self.order_count).rjust(6, "0")
850 |
851 | orderid: str = prefix + suffix
852 | return orderid
853 |
854 | def send_order(self, req: OrderRequest) -> str:
855 | """委托下单"""
856 | contract: ContractData = symbol_contract_map.get(req.vt_symbol, None)
857 | if not contract:
858 | self.gateway.write_log(f"找不到该合约{req.vt_symbol}")
859 | return ""
860 |
861 | if contract.exchange not in {Exchange.SSE, Exchange.SZSE, Exchange.BSE}:
862 | self.gateway.write_log(f"不支持的合约{req.vt_symbol}")
863 | return ""
864 |
865 | if req.type not in {OrderType.LIMIT}:
866 | self.gateway.write_log(f"不支持的委托类型: {req.type.value}")
867 | return ""
868 |
869 | if req.offset == Offset.NONE and contract.product == Product.OPTION:
870 | self.gateway.write_log("委托失败,期权交易需要选择开平方向")
871 | return ""
872 |
873 | stock_code: str = req.symbol + "." + EXCHANGE_VT2XT[req.exchange]
874 | if self.account_type == "STOCK_OPTION":
875 | stock_code += "O"
876 |
877 | # 现货委托不考虑开平
878 | if contract.product == Product.OPTION:
879 | xt_direction: tuple = (req.direction, req.offset)
880 | else:
881 | xt_direction = (req.direction, Offset.NONE)
882 |
883 | orderid: str = self.new_orderid()
884 |
885 | self.xt_client.order_stock_async(
886 | account=self.xt_account,
887 | stock_code=stock_code,
888 | order_type=DIRECTION_VT2XT[xt_direction],
889 | order_volume=int(req.volume),
890 | price_type=ORDERTYPE_VT2XT[(req.exchange, req.type)],
891 | price=req.price,
892 | strategy_name=req.reference,
893 | order_remark=orderid
894 | )
895 |
896 | order: OrderData = req.create_order_data(orderid, self.gateway_name)
897 | self.gateway.on_order(order)
898 |
899 | vt_orderid: str = order.vt_orderid
900 |
901 | return vt_orderid
902 |
903 | def cancel_order(self, req: CancelRequest) -> None:
904 | """委托撤单"""
905 | sysid: str | None = self.active_localid_sysid_map.get(req.orderid, None)
906 | if not sysid:
907 | self.gateway.write_log("撤单失败,找不到委托号")
908 | return
909 |
910 | if req.exchange == Exchange.SSE:
911 | market: int = 0
912 | else:
913 | market = 1
914 |
915 | self.xt_client.cancel_order_stock_sysid_async(self.xt_account, market, sysid)
916 |
917 | def query_position(self) -> None:
918 | """查询持仓"""
919 | if self.connected:
920 | self.xt_client.query_stock_positions_async(self.xt_account, self.on_query_positions_async)
921 |
922 | def query_account(self) -> None:
923 | """查询账户资金"""
924 | if self.connected:
925 | self.xt_client.query_stock_asset_async(self.xt_account, self.on_query_asset_async)
926 |
927 | def query_order(self) -> None:
928 | """查询委托信息"""
929 | if self.connected:
930 | self.xt_client.query_stock_orders_async(self.xt_account, self.on_query_order_async)
931 |
932 | def query_trade(self) -> None:
933 | """查询成交信息"""
934 | if self.connected:
935 | self.xt_client.query_stock_trades_async(self.xt_account, self.on_query_trades_async)
936 |
937 | def close(self) -> None:
938 | """关闭连接"""
939 | if self.inited:
940 | self.xt_client.stop()
941 |
942 |
943 | def generate_datetime(timestamp: int, millisecond: bool = True) -> datetime:
944 | """生成本地时间"""
945 | if millisecond:
946 | dt: datetime = datetime.fromtimestamp(timestamp / 1000)
947 | else:
948 | dt = datetime.fromtimestamp(timestamp)
949 | dt = dt.replace(tzinfo=CHINA_TZ)
950 | return dt
951 |
952 |
953 | def process_etf_option(get_instrument_detail: Callable, xt_symbol: str, gateway_name: str) -> ContractData | None:
954 | """处理ETF期权"""
955 | # 拆分XT代码
956 | symbol, xt_exchange = xt_symbol.split(".")
957 |
958 | # 筛选期权合约合约(ETF期权代码为8位)
959 | if len(symbol) != 8:
960 | return None
961 |
962 | # 查询转换数据
963 | data: dict = get_instrument_detail(xt_symbol, True)
964 |
965 | name: str = data["InstrumentName"]
966 | if "购" in name:
967 | option_type = OptionType.CALL
968 | elif "沽" in name:
969 | option_type = OptionType.PUT
970 | else:
971 | return None
972 |
973 | if "A" in name:
974 | option_index = str(data["OptExercisePrice"]) + "-A"
975 | else:
976 | option_index = str(data["OptExercisePrice"]) + "-M"
977 |
978 | contract: ContractData = ContractData(
979 | symbol=data["InstrumentID"],
980 | exchange=EXCHANGE_XT2VT[xt_exchange],
981 | name=data["InstrumentName"],
982 | product=Product.OPTION,
983 | size=data["VolumeMultiple"],
984 | pricetick=data["PriceTick"],
985 | min_volume=data["MinLimitOrderVolume"],
986 | option_strike=data["OptExercisePrice"],
987 | option_listed=datetime.strptime(data["OpenDate"], "%Y%m%d"),
988 | option_expiry=datetime.strptime(data["ExpireDate"], "%Y%m%d"),
989 | option_portfolio=data["OptUndlCode"] + "_O",
990 | option_index=option_index,
991 | option_type=option_type,
992 | option_underlying=data["OptUndlCode"] + "-" + str(data["ExpireDate"])[:6],
993 | gateway_name=gateway_name
994 | )
995 |
996 | symbol_limit_map[contract.vt_symbol] = (data["UpStopPrice"], data["DownStopPrice"])
997 |
998 | return contract
999 |
1000 |
1001 | def process_futures_option(get_instrument_detail: Callable, xt_symbol: str, gateway_name: str) -> ContractData | None:
1002 | """处理期货期权"""
1003 | # 筛选期权合约
1004 | data: dict = get_instrument_detail(xt_symbol, True)
1005 |
1006 | option_strike: float = data["OptExercisePrice"]
1007 | if not option_strike:
1008 | return None
1009 |
1010 | # 拆分XT代码
1011 | symbol, xt_exchange = xt_symbol.split(".")
1012 |
1013 | # 移除产品前缀
1014 | for _ix, w in enumerate(symbol):
1015 | if w.isdigit():
1016 | break
1017 |
1018 | suffix: str = symbol[_ix:]
1019 |
1020 | # 过滤非期权合约
1021 | if "(" in symbol or " " in symbol:
1022 | return None
1023 |
1024 | # 判断期权类型
1025 | if "C" in suffix:
1026 | option_type = OptionType.CALL
1027 | elif "P" in suffix:
1028 | option_type = OptionType.PUT
1029 | else:
1030 | return None
1031 |
1032 | # 获取期权标的
1033 | if "-" in symbol:
1034 | option_underlying: str = symbol.split("-")[0]
1035 | else:
1036 | option_underlying = data["OptUndlCode"]
1037 |
1038 | # 转换数据
1039 | contract: ContractData = ContractData(
1040 | symbol=data["InstrumentID"],
1041 | exchange=EXCHANGE_XT2VT[xt_exchange],
1042 | name=data["InstrumentName"],
1043 | product=Product.OPTION,
1044 | size=data["VolumeMultiple"],
1045 | pricetick=data["PriceTick"],
1046 | min_volume=data["MinLimitOrderVolume"],
1047 | option_strike=data["OptExercisePrice"],
1048 | option_listed=datetime.strptime(data["OpenDate"], "%Y%m%d"),
1049 | option_expiry=datetime.strptime(data["ExpireDate"], "%Y%m%d"),
1050 | option_index=str(data["OptExercisePrice"]),
1051 | option_type=option_type,
1052 | option_underlying=option_underlying,
1053 | gateway_name=gateway_name
1054 | )
1055 |
1056 | if contract.exchange == Exchange.CZCE:
1057 | contract.option_portfolio = data["ProductID"][:-1]
1058 | else:
1059 | contract.option_portfolio = data["ProductID"]
1060 |
1061 | symbol_limit_map[contract.vt_symbol] = (data["UpStopPrice"], data["DownStopPrice"])
1062 |
1063 | return contract
1064 |
--------------------------------------------------------------------------------