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