diff --git a/run_web.py b/run_web.py index dd585a7..923bd77 100644 --- a/run_web.py +++ b/run_web.py @@ -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 diff --git a/vnpy_qmt_v0.3.3/LICENSE b/vnpy_qmt_v0.3.3/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/vnpy_qmt_v0.3.3/LICENSE @@ -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. diff --git a/vnpy_qmt_v0.3.3/README.md b/vnpy_qmt_v0.3.3/README.md new file mode 100644 index 0000000..0a5fdd4 --- /dev/null +++ b/vnpy_qmt_v0.3.3/README.md @@ -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 核对接口变更 +``` diff --git a/vnpy_qmt_v0.3.3/vnpy_qmt/__init__.py b/vnpy_qmt_v0.3.3/vnpy_qmt/__init__.py new file mode 100644 index 0000000..0f92670 --- /dev/null +++ b/vnpy_qmt_v0.3.3/vnpy_qmt/__init__.py @@ -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 diff --git a/vnpy_qmt_v0.3.3/vnpy_qmt/md.py b/vnpy_qmt_v0.3.3/vnpy_qmt/md.py new file mode 100644 index 0000000..66ef5ab --- /dev/null +++ b/vnpy_qmt_v0.3.3/vnpy_qmt/md.py @@ -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}") \ No newline at end of file diff --git a/vnpy_qmt_v0.3.3/vnpy_qmt/qmt_gateway.py b/vnpy_qmt_v0.3.3/vnpy_qmt/qmt_gateway.py new file mode 100644 index 0000000..1580e47 --- /dev/null +++ b/vnpy_qmt_v0.3.3/vnpy_qmt/qmt_gateway.py @@ -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() \ No newline at end of file diff --git a/vnpy_qmt_v0.3.3/vnpy_qmt/td.py b/vnpy_qmt_v0.3.3/vnpy_qmt/td.py new file mode 100644 index 0000000..9564e13 --- /dev/null +++ b/vnpy_qmt_v0.3.3/vnpy_qmt/td.py @@ -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}') diff --git a/vnpy_qmt_v0.3.3/vnpy_qmt/utils.py b/vnpy_qmt_v0.3.3/vnpy_qmt/utils.py new file mode 100644 index 0000000..0768bd6 --- /dev/null +++ b/vnpy_qmt_v0.3.3/vnpy_qmt/utils.py @@ -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) \ No newline at end of file