chore(vendor): vendor vnpy_qmt 0.3.3 源码自行维护
vnpy_qmt 是唯一券商交易 gateway(关键路径),社区包断更有风险。 按 vnpy_v4.4.0 同一套约定 vendor 源码、sys.path 引用、不 pip install。 - vnpy_qmt_v0.3.3/vnpy_qmt/ (5文件587行) + LICENSE(Apache-2.0) + README - run_web.py 加 if-exists 路径块,import 方式不变 (from vnpy_qmt import QmtGateway) Mac 验证: py_compile 全过 + import 解析到 vendor 副本(仅卡 xtquant Win-only,符合预期) VPS 生产切换(Part B: rsync+pip uninstall+重启+实证) 待确认
This commit is contained in:
@@ -15,6 +15,12 @@ if os.path.exists(vnpy_dir):
|
||||
sys.path.insert(0, vnpy_dir)
|
||||
print(f"Added vnpy_v4.4.0 to Python path: {vnpy_dir}")
|
||||
|
||||
# 添加 vnpy_qmt_v0.3.3(vendor 源码,自行维护)到 Python 路径
|
||||
qmt_dir = os.path.join(project_dir, "vnpy_qmt_v0.3.3")
|
||||
if os.path.exists(qmt_dir):
|
||||
sys.path.insert(0, qmt_dir)
|
||||
print(f"Added vnpy_qmt_v0.3.3 to Python path: {qmt_dir}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,39 @@
|
||||
# vnpy_qmt v0.3.3(vendored,自行维护)
|
||||
|
||||
本目录是 **vnpy-qmt 0.3.3** 的源码 vendor 副本,纳入本仓库自行维护。
|
||||
|
||||
## 来源
|
||||
- PyPI:`vnpy-qmt==0.3.3`(wheel:`vnpy_qmt-0.3.3-py3-none-any.whl`)
|
||||
- 上游:社区项目 `ruyisee/vnpy_qmt`(vnpy 的 miniQMT/xtquant 交易 Gateway)
|
||||
- License:**Apache License 2.0**(见 `LICENSE`,vnpy 生态统一协议)
|
||||
|
||||
## 为什么 vendor(不 pip install)
|
||||
`vnpy_qmt` 是本项目**唯一的券商交易 Gateway**(brain → vnpy_qmt → xtquant → miniQMT 下单链路的关键一环)。
|
||||
社区包存在断更/失效风险,关键路径依赖外部仓库不可控。vendor 源码后,我们能自主修 bug、跟进 vnpy 接口变更。
|
||||
|
||||
与同目录的 `vnpy_v4.4.0/` 采用**同一套约定**:源码 vendor、`sys.path` 引用、不 pip install。
|
||||
|
||||
## 如何引用
|
||||
`run_web.py` 启动时把本目录加入 `sys.path`(与 `vnpy_v4.4.0` 同样的 if-exists 块):
|
||||
|
||||
```python
|
||||
qmt_dir = os.path.join(project_dir, "vnpy_qmt_v0.3.3")
|
||||
if os.path.exists(qmt_dir):
|
||||
sys.path.insert(0, qmt_dir)
|
||||
```
|
||||
|
||||
业务代码 import 方式**不变**:
|
||||
```python
|
||||
from vnpy_qmt import QmtGateway
|
||||
```
|
||||
|
||||
## 维护注意
|
||||
- vnpy 接口兼容性已验证(对 `vnpy_v4.4.0` 零漂移,2026-07 真机实证)。后续若升级 vnpy,需重新核对 `BaseGateway`/`OrderData`/`Status` 枚举/`ZoneInfo` 等 import。
|
||||
- `xtquant` 是国金 QMT 官方库(行情 `xtdata` + 交易 `xttrader`),**仅 Windows + miniQMT 进程常驻**可用——本目录不包含 xtquant,Mac/NAS 无法完整 import(会卡在 `from xtquant import ...`),属正常。
|
||||
- 文件清单:`vnpy_qmt/__init__.py`(导出 QmtGateway)/ `qmt_gateway.py`(主 Gateway)/ `td.py`(交易)/ `md.py`(行情)/ `utils.py`。
|
||||
|
||||
## 升级方法
|
||||
```
|
||||
pip download vnpy-qmt==<新版本> --no-deps -d /tmp/dl
|
||||
# 解压 wheel,用 vnpy_qmt/ 覆盖本目录,更新本 README 版本号,diff 核对接口变更
|
||||
```
|
||||
@@ -0,0 +1,7 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
"""
|
||||
@FileName :__init__.py.py
|
||||
@Time :2022/11/8 16:48
|
||||
@Author :fsksf
|
||||
"""
|
||||
from vnpy_qmt.qmt_gateway import QmtGateway
|
||||
@@ -0,0 +1,150 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
"""
|
||||
@FileName :md.py
|
||||
@Time :2022/11/8 17:14
|
||||
@Author :fsksf
|
||||
"""
|
||||
|
||||
from vnpy.trader.object import (
|
||||
CancelRequest, OrderRequest, SubscribeRequest, TickData,
|
||||
ContractData
|
||||
)
|
||||
import xtquant.xtdata
|
||||
import xtquant.xttrader
|
||||
import xtquant.xttype
|
||||
from vnpy_qmt.utils import (
|
||||
From_VN_Exchange_map, TO_VN_Exchange_map, to_vn_contract,
|
||||
TO_VN_Product, to_vn_product, timestamp_to_datetime,
|
||||
to_qmt_code
|
||||
)
|
||||
from vnpy.trader.utility import ZoneInfo
|
||||
|
||||
ZONE_INFO = ZoneInfo("Asia/Shanghai")
|
||||
|
||||
|
||||
class MD:
|
||||
|
||||
def __init__(self, gateway):
|
||||
self.gateway = gateway
|
||||
self.th = None
|
||||
self.limit_ups = {}
|
||||
self.limit_downs = {}
|
||||
|
||||
def close(self) -> None:
|
||||
pass
|
||||
|
||||
def subscribe(self, req: SubscribeRequest) -> None:
|
||||
|
||||
return xtquant.xtdata.subscribe_quote(
|
||||
stock_code=f'{req.symbol}.{From_VN_Exchange_map[req.exchange]}',
|
||||
period='tick',
|
||||
callback=self.on_tick
|
||||
)
|
||||
|
||||
def connect(self, setting: dict) -> None:
|
||||
self.get_contract()
|
||||
return
|
||||
|
||||
def get_contract(self):
|
||||
self.write_log('开始获取标的信息')
|
||||
contract_ids = set()
|
||||
bk = ['上期所', '上证A股', '上证B股', '中金所', '创业板', '大商所',
|
||||
'沪市ETF', '沪市指数', '沪深A股',
|
||||
'沪深B股', '沪深ETF', '沪深指数', '深市ETF',
|
||||
'深市基金', '深市指数', '深证A股', '深证B股', '科创板', '科创板CDR',
|
||||
]
|
||||
for sector in bk:
|
||||
print(sector)
|
||||
stock_list = xtquant.xtdata.get_stock_list_in_sector(sector_name=sector)
|
||||
for symbol in stock_list:
|
||||
if symbol in contract_ids:
|
||||
continue
|
||||
contract_ids.add(symbol)
|
||||
info = xtquant.xtdata.get_instrument_detail(symbol)
|
||||
contract_type = xtquant.xtdata.get_instrument_type(symbol)
|
||||
if info is None or contract_type is None:
|
||||
continue
|
||||
try:
|
||||
exchange = TO_VN_Exchange_map[info['ExchangeID']]
|
||||
except KeyError:
|
||||
|
||||
print('本gateway不支持的标的', symbol)
|
||||
continue
|
||||
if exchange not in self.gateway.exchanges:
|
||||
continue
|
||||
product = to_vn_product(contract_type)
|
||||
if product not in self.gateway.TRADE_TYPE:
|
||||
continue
|
||||
|
||||
c = ContractData(
|
||||
gateway_name=self.gateway.gateway_name,
|
||||
symbol=info['InstrumentID'],
|
||||
exchange=exchange,
|
||||
name=info['InstrumentName'],
|
||||
product=product,
|
||||
pricetick=info['PriceTick'],
|
||||
size=100,
|
||||
min_volume=100
|
||||
)
|
||||
self.limit_ups[c.vt_symbol] = info['UpStopPrice']
|
||||
self.limit_downs[c.vt_symbol] = info['DownStopPrice']
|
||||
self.gateway.on_contract(c)
|
||||
self.write_log('获取标的信息完成')
|
||||
|
||||
def on_tick(self, datas):
|
||||
for code, data_list in datas.items():
|
||||
symbol, suffix = code.rsplit('.')
|
||||
exchange = TO_VN_Exchange_map[suffix]
|
||||
for data in data_list:
|
||||
ask_price = data['askPrice']
|
||||
ask_vol = data['askVol']
|
||||
bid_price = data['bidPrice']
|
||||
bid_vol = data['bidVol']
|
||||
dt = timestamp_to_datetime(data['time'])
|
||||
dt = dt.replace(tzinfo=ZONE_INFO)
|
||||
tick = TickData(
|
||||
gateway_name=self.gateway.gateway_name,
|
||||
symbol=symbol,
|
||||
exchange=exchange,
|
||||
datetime=dt,
|
||||
last_price=data['lastPrice'],
|
||||
volume=data['volume'],
|
||||
open_price=data['open'],
|
||||
high_price=data['high'],
|
||||
low_price=data['low'],
|
||||
pre_close=data['lastClose'],
|
||||
limit_down=0,
|
||||
limit_up=0,
|
||||
ask_price_1=ask_price[0],
|
||||
ask_price_2=ask_price[1],
|
||||
ask_price_3=ask_price[2],
|
||||
ask_price_4=ask_price[3],
|
||||
ask_price_5=ask_price[4],
|
||||
|
||||
ask_volume_1=ask_vol[0],
|
||||
ask_volume_2=ask_vol[1],
|
||||
ask_volume_3=ask_vol[2],
|
||||
ask_volume_4=ask_vol[3],
|
||||
ask_volume_5=ask_vol[4],
|
||||
|
||||
bid_price_1=bid_price[0],
|
||||
bid_price_2=bid_price[1],
|
||||
bid_price_3=bid_price[2],
|
||||
bid_price_4=bid_price[3],
|
||||
bid_price_5=bid_price[4],
|
||||
|
||||
bid_volume_1=bid_vol[0],
|
||||
bid_volume_2=bid_vol[1],
|
||||
bid_volume_3=bid_vol[2],
|
||||
bid_volume_4=bid_vol[3],
|
||||
bid_volume_5=bid_vol[4],
|
||||
)
|
||||
contract = self.gateway.get_contract(tick.vt_symbol)
|
||||
if contract:
|
||||
tick.name = contract.name
|
||||
tick.limit_up = self.limit_ups.get(tick.vt_symbol, None)
|
||||
tick.limit_down = self.limit_downs.get(tick.vt_symbol, None)
|
||||
self.gateway.on_tick(tick)
|
||||
|
||||
def write_log(self, msg):
|
||||
self.gateway.write_log(f"[ md ] {msg}")
|
||||
@@ -0,0 +1,118 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
"""
|
||||
@FileName :qmt_gateway.py
|
||||
@Time :2022/11/8 16:49
|
||||
@Author :fsksf
|
||||
"""
|
||||
from typing import Dict, List
|
||||
from vnpy.event import Event, EventEngine
|
||||
from vnpy.trader.event import (
|
||||
EVENT_TIMER,
|
||||
EVENT_TICK
|
||||
)
|
||||
from vnpy.trader.constant import (
|
||||
Product, Direction, OrderType, Exchange
|
||||
|
||||
)
|
||||
from vnpy.trader.gateway import BaseGateway
|
||||
from vnpy.trader.object import (
|
||||
OrderRequest,
|
||||
CancelRequest,
|
||||
SubscribeRequest,
|
||||
ContractData,
|
||||
)
|
||||
|
||||
from vnpy_qmt.md import MD
|
||||
from vnpy_qmt.td import TD
|
||||
|
||||
|
||||
class QmtGateway(BaseGateway):
|
||||
|
||||
default_name = "QMT"
|
||||
|
||||
default_setting: Dict[str, str] = {
|
||||
"交易账号": "",
|
||||
"mini路径": ""
|
||||
}
|
||||
|
||||
TRADE_TYPE = (Product.ETF, Product.EQUITY, Product.BOND, Product.INDEX)
|
||||
exchanges = (Exchange.SSE, Exchange.SZSE)
|
||||
|
||||
def __init__(self, event_engine: EventEngine, gateway_name: str = 'QMT'):
|
||||
super(QmtGateway, self).__init__(event_engine, gateway_name)
|
||||
self.contracts: Dict[str, ContractData] = {}
|
||||
self.md = MD(self)
|
||||
self.td = TD(self)
|
||||
self.count = -1
|
||||
self.event_engine.register(EVENT_TIMER, self.process_timer_event)
|
||||
|
||||
def connect(self, setting: dict) -> None:
|
||||
self.md.connect(setting)
|
||||
self.td.connect(setting)
|
||||
|
||||
def close(self) -> None:
|
||||
self.md.close()
|
||||
|
||||
def subscribe(self, req: SubscribeRequest) -> None:
|
||||
return self.md.subscribe(req)
|
||||
|
||||
def send_order(self, req: OrderRequest) -> str:
|
||||
return self.td.send_order(req)
|
||||
|
||||
def cancel_order(self, req: CancelRequest) -> None:
|
||||
return self.td.cancel_order(req.orderid)
|
||||
|
||||
def query_account(self) -> None:
|
||||
self.td.query_account()
|
||||
|
||||
def query_position(self) -> None:
|
||||
self.td.query_position()
|
||||
|
||||
def query_order(self):
|
||||
self.td.query_order()
|
||||
|
||||
def query_trade(self):
|
||||
self.td.query_trade()
|
||||
|
||||
def on_contract(self, contract):
|
||||
self.contracts[contract.vt_symbol] = contract
|
||||
super(QmtGateway, self).on_contract(contract)
|
||||
|
||||
def get_contract(self, vt_symbol):
|
||||
return self.contracts.get(vt_symbol)
|
||||
|
||||
def process_timer_event(self, event) -> None:
|
||||
if not self.td.inited:
|
||||
return
|
||||
if self.count == -1:
|
||||
self.query_trade()
|
||||
self.count += 1
|
||||
|
||||
if self.count % 5 == 0:
|
||||
self.query_order()
|
||||
|
||||
if self.count % 7 == 0:
|
||||
self.query_account()
|
||||
self.query_position()
|
||||
if self.count < 21:
|
||||
return
|
||||
self.count = 0
|
||||
|
||||
def write_log(self, msg):
|
||||
super(QmtGateway, self).write_log(f"[QMT] {msg}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
qmt = QmtGateway(None)
|
||||
qmt.subscribe(SubscribeRequest(symbol='000001', exchange=Exchange.SZSE))
|
||||
qmt.md.get_contract()
|
||||
|
||||
import threading
|
||||
import time
|
||||
|
||||
def slp():
|
||||
while True:
|
||||
time.sleep(0.1)
|
||||
t = threading.Thread(target=slp)
|
||||
t.start()
|
||||
t.join()
|
||||
@@ -0,0 +1,227 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
"""
|
||||
@FileName :td.py
|
||||
@Time :2022/11/8 17:14
|
||||
@Author :fsksf
|
||||
"""
|
||||
import os
|
||||
import random
|
||||
from typing import Dict
|
||||
import datetime
|
||||
|
||||
from xtquant.xttrader import XtQuantTraderCallback, XtQuantTrader
|
||||
from xtquant.xttype import (
|
||||
XtTrade, XtAsset, XtOrder, XtOrderError, XtCreditOrder, XtOrderResponse,
|
||||
XtPosition, XtCreditDeal, XtCancelError, XtCancelOrderResponse, StockAccount
|
||||
)
|
||||
from vnpy.trader.constant import Direction, Status, Product
|
||||
from vnpy.trader.object import (
|
||||
AccountData, TradeData, OrderData, OrderRequest, PositionData
|
||||
)
|
||||
from vnpy_qmt.utils import (to_vn_product, to_vn_contract, to_qmt_code,
|
||||
From_VN_Trade_Type, from_vn_price_type, TO_VN_Trade_Type,
|
||||
timestamp_to_datetime, TO_VN_ORDER_STATUS)
|
||||
|
||||
|
||||
class TD(XtQuantTraderCallback):
|
||||
|
||||
def __init__(self, gateway, *args, **kwargs):
|
||||
super(TD, self).__init__(*args, **kwargs)
|
||||
self.gateway = gateway
|
||||
self.count = 0
|
||||
self.session_id = int(datetime.datetime.now().strftime('%H%M%S'))
|
||||
|
||||
self.trader: XtQuantTrader = None
|
||||
self.account = None
|
||||
self.mini_path = None
|
||||
self.inited = False
|
||||
self.orders: Dict[str, OrderData] = {}
|
||||
self.traders: Dict[str, TradeData] = {}
|
||||
|
||||
def connect(self, settings: dict):
|
||||
account = settings['交易账号']
|
||||
self.mini_path = path = settings['mini路径']
|
||||
acc = StockAccount(account)
|
||||
self.account = acc
|
||||
self.trader = XtQuantTrader(path=path, session=self.session_id)
|
||||
self.trader.register_callback(self)
|
||||
self.trader.start()
|
||||
self.write_log('连接QMT')
|
||||
cnn_msg = self.trader.connect()
|
||||
if cnn_msg == 0:
|
||||
self.write_log('连接成功')
|
||||
else:
|
||||
self.write_log(f'连接失败:{cnn_msg}')
|
||||
sub_msg = self.trader.subscribe(account=acc)
|
||||
if sub_msg == 0:
|
||||
self.write_log(f'订阅账户成功: {sub_msg}')
|
||||
self.inited = True
|
||||
else:
|
||||
self.write_log(f'订阅账户【失败】: {sub_msg}')
|
||||
|
||||
def get_order_remark(self):
|
||||
self.count += 1
|
||||
mark = f'{str(self.session_id)}#{self.count}'
|
||||
return mark
|
||||
|
||||
def send_order(self, req: OrderRequest):
|
||||
vn_oid = self.get_order_remark()
|
||||
seq = self.trader.order_stock_async(
|
||||
account=self.account,
|
||||
stock_code=to_qmt_code(symbol=req.symbol, exchange=req.exchange),
|
||||
order_type=From_VN_Trade_Type[req.direction],
|
||||
price_type=from_vn_price_type(req),
|
||||
order_volume=int(req.volume),
|
||||
price=req.price,
|
||||
order_remark=vn_oid,
|
||||
)
|
||||
order = OrderData(gateway_name=self.gateway.gateway_name,
|
||||
symbol=req.symbol,
|
||||
exchange=req.exchange,
|
||||
orderid=vn_oid,
|
||||
type=req.type,
|
||||
direction=req.direction,
|
||||
offset=req.offset,
|
||||
volume=req.volume,
|
||||
price=req.price,
|
||||
status=Status.SUBMITTING)
|
||||
self.orders[order.orderid] = order
|
||||
return order.vt_orderid
|
||||
|
||||
def cancel_order(self, order_id):
|
||||
order = self.orders.get(order_id)
|
||||
if order is None:
|
||||
return
|
||||
return self.trader.cancel_order_stock_async(account=self.account, order_id=order.reference)
|
||||
|
||||
def query_account(self):
|
||||
return self.trader.query_stock_asset_async(self.account, callback=self.on_stock_asset)
|
||||
|
||||
def query_position(self):
|
||||
return self.trader.query_stock_positions_async(self.account, callback=self.on_stock_positions_callback)
|
||||
|
||||
def query_order(self):
|
||||
self.trader.query_stock_orders_async(self.account, callback=self.on_stock_order_callback)
|
||||
|
||||
def query_trade(self):
|
||||
self.trader.query_stock_trades_async(self.account, callback=self.on_stock_trade_callback)
|
||||
|
||||
def on_disconnected(self):
|
||||
pass
|
||||
|
||||
def on_stock_asset(self, asset: XtAsset):
|
||||
account = AccountData(
|
||||
accountid=asset.account_id,
|
||||
frozen=asset.frozen_cash,
|
||||
balance=asset.total_asset,
|
||||
gateway_name=self.gateway.gateway_name
|
||||
)
|
||||
self.gateway.on_account(account)
|
||||
|
||||
def on_stock_order_callback(self, order_list):
|
||||
for order in order_list:
|
||||
self.on_stock_order(order)
|
||||
|
||||
def on_stock_positions_callback(self, pos_list):
|
||||
for pos in pos_list:
|
||||
self.on_stock_position(pos)
|
||||
|
||||
def on_stock_trade_callback(self, trade_list):
|
||||
for trade in trade_list:
|
||||
self.on_stock_trade(trade)
|
||||
|
||||
def on_stock_order(self, order: XtOrder):
|
||||
symbol, exchange = to_vn_contract(order.stock_code)
|
||||
remark_id = order.order_remark
|
||||
vn_order = OrderData(
|
||||
orderid=remark_id,
|
||||
symbol=symbol,
|
||||
exchange=exchange,
|
||||
price=order.price,
|
||||
volume=order.order_volume,
|
||||
traded=order.traded_volume,
|
||||
gateway_name=self.gateway.gateway_name,
|
||||
status=TO_VN_ORDER_STATUS[order.order_status],
|
||||
direction=TO_VN_Trade_Type[order.order_type],
|
||||
datetime=timestamp_to_datetime(order.order_time),
|
||||
reference=order.order_id
|
||||
)
|
||||
old_order = self.orders.get(vn_order.orderid, None)
|
||||
if old_order == vn_order:
|
||||
return
|
||||
if vn_order.status == Status.REJECTED:
|
||||
self.write_log(f'【拒单】 {order.status_msg}')
|
||||
self.orders[vn_order.orderid] = vn_order
|
||||
self.gateway.on_order(vn_order)
|
||||
|
||||
def on_stock_position(self, position: XtPosition):
|
||||
try:
|
||||
symbol, exchange = to_vn_contract(position.stock_code)
|
||||
except Exception as e:
|
||||
print(f"on_stock_position 无法解析的代码: {position.stock_code}")
|
||||
return
|
||||
# TODO ETF相关字段处理
|
||||
position_ = PositionData(
|
||||
gateway_name=self.gateway.gateway_name,
|
||||
symbol=symbol,
|
||||
exchange=exchange,
|
||||
direction=Direction.LONG,
|
||||
volume=position.volume,
|
||||
yd_volume=position.yesterday_volume,
|
||||
price=position.open_price,
|
||||
pnl=position.market_value - position.volume * position.open_price
|
||||
)
|
||||
contract = self.gateway.get_contract(position_.vt_symbol)
|
||||
if contract:
|
||||
position_.product = contract.product
|
||||
position_.__post_init__()
|
||||
self.gateway.on_position(position_)
|
||||
|
||||
def on_stock_trade(self, trade: XtTrade):
|
||||
symbol, exchange = to_vn_contract(trade.stock_code)
|
||||
vn_oid = trade.order_remark
|
||||
if vn_oid is None:
|
||||
return
|
||||
order = self.orders.get(vn_oid)
|
||||
if order is None:
|
||||
return
|
||||
trd_typ = TO_VN_Trade_Type[trade.order_type]
|
||||
trade_ = TradeData(
|
||||
gateway_name=self.gateway.gateway_name,
|
||||
symbol=symbol,
|
||||
exchange=exchange,
|
||||
orderid=vn_oid,
|
||||
tradeid=trade.traded_id,
|
||||
price=trade.traded_price,
|
||||
datetime=timestamp_to_datetime(trade.traded_time),
|
||||
volume=trade.traded_volume,
|
||||
direction=trd_typ
|
||||
)
|
||||
|
||||
self.gateway.on_trade(trade_)
|
||||
|
||||
def on_cancel_error(self, cancel_error: XtCancelError):
|
||||
self.write_log(cancel_error.error_msg)
|
||||
|
||||
def on_order_error(self, order_error: XtOrderError):
|
||||
self.write_log(f'订单错误:{order_error.error_msg}')
|
||||
vn_oid = order_error.order_remark
|
||||
old_order = self.orders.get(vn_oid)
|
||||
if old_order:
|
||||
old_order.status = Status.REJECTED
|
||||
self.gateway.on_order(old_order)
|
||||
|
||||
def on_order_stock_async_response(self, response: XtOrderResponse):
|
||||
self.write_log(f'下单成功 {response.order_id} {response.order_remark} {response.strategy_name}')
|
||||
old_order = self.orders.get(response.order_remark)
|
||||
if old_order:
|
||||
if response.error_msg:
|
||||
old_order.status = Status.REJECTED
|
||||
self.write_log(f'下单失败 {response.order_remark} 原因: {response.error_msg}')
|
||||
self.gateway.on_order(old_order)
|
||||
|
||||
def on_cancel_order_stock_async_response(self, response: XtCancelOrderResponse):
|
||||
self.write_log(f'撤单结果: {response.cancel_result}')
|
||||
|
||||
def write_log(self, msg):
|
||||
self.gateway.write_log(f'[ td ] {msg}')
|
||||
@@ -0,0 +1,88 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
"""
|
||||
@FileName :utils.py
|
||||
@Time :2022/11/8 17:07
|
||||
@Author :fsksf
|
||||
"""
|
||||
import datetime
|
||||
from vnpy.trader.object import OrderRequest
|
||||
from vnpy.trader.constant import Exchange, Product, OrderType, Direction, Status
|
||||
from xtquant import xtconstant
|
||||
|
||||
From_VN_Exchange_map = {
|
||||
Exchange.CFFEX: 'CFF',
|
||||
Exchange.SSE: 'SH',
|
||||
Exchange.SZSE: 'SZ',
|
||||
Exchange.SHFE: 'SHF',
|
||||
Exchange.CZCE: 'CZC',
|
||||
Exchange.DCE: 'DCE',
|
||||
}
|
||||
|
||||
TO_VN_Exchange_map = {v: k for k, v in From_VN_Exchange_map.items()}
|
||||
|
||||
|
||||
From_VN_Trade_Type = {
|
||||
Direction.LONG: xtconstant.STOCK_BUY,
|
||||
Direction.SHORT: xtconstant.STOCK_SELL,
|
||||
}
|
||||
|
||||
|
||||
TO_VN_Trade_Type = {v: k for k, v in From_VN_Trade_Type.items()}
|
||||
|
||||
|
||||
TO_VN_ORDER_STATUS = {
|
||||
xtconstant.ORDER_UNREPORTED: Status.SUBMITTING,
|
||||
xtconstant.ORDER_WAIT_REPORTING: Status.SUBMITTING,
|
||||
xtconstant.ORDER_REPORTED: Status.NOTTRADED,
|
||||
xtconstant.ORDER_REPORTED_CANCEL: Status.NOTTRADED,
|
||||
xtconstant.ORDER_PARTSUCC_CANCEL: Status.PARTTRADED,
|
||||
xtconstant.ORDER_PART_CANCEL: Status.CANCELLED,
|
||||
xtconstant.ORDER_CANCELED: Status.CANCELLED,
|
||||
xtconstant.ORDER_PART_SUCC: Status.PARTTRADED,
|
||||
xtconstant.ORDER_SUCCEEDED: Status.ALLTRADED,
|
||||
xtconstant.ORDER_JUNK: Status.REJECTED,
|
||||
xtconstant.ORDER_UNKNOWN: Status.REJECTED
|
||||
}
|
||||
|
||||
|
||||
def from_vn_price_type(req: OrderRequest):
|
||||
if req.type == OrderType.LIMIT:
|
||||
return xtconstant.FIX_PRICE
|
||||
elif req.type == OrderType.MARKET:
|
||||
return xtconstant.LATEST_PRICE
|
||||
|
||||
|
||||
def to_vn_contract(symbol):
|
||||
code, suffix = symbol.rsplit('.')
|
||||
exchange = TO_VN_Exchange_map[suffix]
|
||||
return code, exchange
|
||||
|
||||
|
||||
TO_VN_Product = {
|
||||
'index': Product.INDEX,
|
||||
'stock': Product.EQUITY,
|
||||
'fund': Product.FUND,
|
||||
'etf': Product.ETF,
|
||||
}
|
||||
|
||||
|
||||
def to_vn_product(dic: dict):
|
||||
if dic.get('etf'):
|
||||
return Product.ETF
|
||||
for k, v in dic.items():
|
||||
if v:
|
||||
break
|
||||
return TO_VN_Product[k]
|
||||
|
||||
|
||||
def to_qmt_code(symbol, exchange):
|
||||
suffix = From_VN_Exchange_map[exchange]
|
||||
return f'{symbol}.{suffix}'
|
||||
|
||||
|
||||
def timestamp_to_datetime(tint):
|
||||
st = len(str(tint))
|
||||
if st != 10:
|
||||
p = st - 10
|
||||
tint = tint / 10**p
|
||||
return datetime.datetime.fromtimestamp(tint)
|
||||
Reference in New Issue
Block a user