QMT 交易接口 (xtquant) 示例代码 平时用的实盘代码用大模型重构后分享

# -*- coding: utf-8 -*-
"""
QMT 交易接口 (xtquant) 示例代码

此代码提供了一个使用中信建投 QMT (xtquant) API 进行交易操作的基础框架。
主要功能包括:
1. 初始化和连接交易服务器。
2. 查询账户资金。
3. 查询账户持仓。
4. 查询当日委托。
5. 查询可撤委托。
6. 下单(买入、卖出、融资买入)。
7. 撤单。

注意:
- 使用前请确保已安装 xtquant 库 (`pip install xtquant`).
- 需要将 `path` 修改为实际的 QMT `userdata_mini` 文件夹路径。
- 需要将 `accoundN` 和 `account_name` 修改为实际的账户信息。
- 融资融券相关功能(查询标的、融资买入)需要信用账户支持。
- 移除了原代码中的 Redis、爬虫(sina)、自定义日志(disp)等外部依赖,使其更专注于 QMT API 本身。
- 错误处理和日志记录已简化,实际使用中应增强。
"""

import time
import random
import warnings
import pandas as pd
import os # 用于检查文件是否存在

# QMT 相关库
from xtquant.xttrader import XtQuantTrader, XtQuantTraderCallback
from xtquant.xttype import StockAccount
from xtquant import xtconstant

warnings.filterwarnings("ignore") # 关闭部分警告信息

# --- QMT 交易回调处理 ---
class MyXtQuantTraderCallback(XtQuantTraderCallback):
    """自定义的 QMT 交易回调类"""
    def on_disconnected(self):
        """连接断开回调"""
        print("交易服务器连接断开")

    def on_stock_order(self, order):
        """
        委托信息推送回调 (包括委托状态更新)
        :param order: XtOrder 对象
        """
        # print(f"委托回报: {order.stock_code}, 委托ID: {order.order_id}, 状态: {order.order_status}, 委托数量: {order.order_volume}, 成交数量: {order.traded_volume}, 委托价格: {order.price}")
        pass # 可以根据需要添加处理逻辑,例如更新内部委托状态

    def on_order_error(self, order_error):
        """
        委托失败推送回调
        :param order_error: XtOrderError 对象
        """
        print(f"!!! 委托失败: 委托ID: {order_error.order_id}, 错误ID: {order_error.error_id}, 错误信息: {order_error.error_msg}")
        # 可以根据需要添加处理逻辑,例如记录失败日志

# --- QMT 交易 API 封装 ---
class TradeApiQmt:
    """封装 QMT 交易接口"""
    def __init__(self):
        """初始化"""
        self.path = ""           # QMT userdata_mini 路径 (需要用户设置)
        self.accoundN = ""       # 交易账户号码 (需要用户设置)
        self.account_name = ""   # 自定义账户名称 (用于区分, 需要用户设置)
        self.account_type = 'STOCK' # 账户类型, 'STOCK' 或 'CREDIT' (会根据 account_name 自动判断)

        self.融资列表 = []        # 存储融资标的列表
        self.session_id = random.randint(10000000, 99999999) # 随机 session id
        self.xt_trader = None    # QMT Trader 实例
        self.acc = None          # QMT StockAccount 实例
        self.callback = None     # 回调处理实例
        self.connected = False   # 连接状态

    def set_account(self, path, account_id, account_name):
        """
        设置账户信息

        :param path: QMT userdata_mini 文件夹路径 (例如: 'D:\\中信证券QMT交易终端\\userdata_mini')
        :param account_id: 交易账户号码 (字符串)
        :param account_name: 自定义账户名 (例如: '模拟普通', '实盘信用')
        """
        self.path = path
        self.accoundN = str(account_id) # 确保是字符串
        self.account_name = account_name
        print(f"账户信息已设置: 路径='{self.path}', 账号='{self.accoundN}', 名称='{self.account_name}'")

    def 初始化(self):
        """
        初始化 QMT 连接
        :return: True 如果成功连接, False 如果失败
        """
        if not self.path or not self.accoundN:
            print("错误:请先调用 set_account 设置账户信息!")
            return False

        # 每次初始化生成新的 session_id
        self.session_id += 1
        print(f"使用 Session ID: {self.session_id} 初始化交易实例...")

        # 创建 Trader 实例
        self.xt_trader = XtQuantTrader(self.path, self.session_id)

        # 判断账户类型
        if '信用' in self.account_name.lower() or 'credit' in self.account_name.lower():
            self.account_type = 'CREDIT'
            self.acc = StockAccount(self.accoundN, account_type='CREDIT')
            print(f"检测到信用账户: {self.accoundN}")
        else:
            self.account_type = 'STOCK'
            self.acc = StockAccount(self.accoundN)
            print(f"使用普通账户: {self.accoundN}")

        # 注册回调
        self.callback = MyXtQuantTraderCallback()
        self.xt_trader.register_callback(self.callback)

        # 启动交易线程
        print("启动交易线程...")
        self.xt_trader.start()

        # 连接交易服务器 (尝试最多3次)
        print("尝试连接交易服务器...")
        connect_result = -1
        for i in range(3):
            connect_result = self.xt_trader.connect()
            if connect_result == 0:
                print("交易服务器连接成功!")
                self.connected = True
                # 连接成功后,查询融资标的 (如果是信用账户)
                self.查询融资融券标的()
                return True
            else:
                print(f"连接失败 (尝试 {i+1}/3),错误码: {connect_result},等待 3 秒后重试...")
                time.sleep(3)
                # 如果连接失败可能需要重启start?文档没明确说明,但加上试试
                if i < 2:
                     print("尝试重启交易线程并重新连接...")
                     self.xt_trader.stop() # 先停止
                     time.sleep(1)
                     self.xt_trader.start() # 再启动

        print(f"!!! 交易服务器连接失败,最终错误码: {connect_result}")
        self.connected = False
        self.xt_trader.stop() # 确保停止
        return False

    def 查询融资融券标的(self):
        """
        查询融资融券标的列表 (仅信用账户有效)
        :return: pandas DataFrame 包含标的信息, 如果不是信用账户或查询失败则返回空 DataFrame
        """
        dfD = pd.DataFrame() # 初始化为空 DataFrame
        if self.account_type == 'CREDIT':
            print("查询融资融券标的...")
            try:
                asset = self.xt_trader.query_credit_subjects(self.acc)
                if asset:
                    listD = []
                    for xx in asset:
                        # 兼容可能不存在的属性
                        融资状态 = getattr(xx, 'fin_status', None)
                        融资保证金比例 = getattr(xx, 'fin_ratio', None)
                        证券市场 = getattr(xx, 'exchange_id', None)
                        证券代码 = getattr(xx, 'instrument_id', '')[:6] # 取前6位

                        if 证券代码 and 证券代码[0] in ['6', '3', '0', '8']: # A股常见代码开头
                            listD.append([证券代码, 证券市场, 融资状态, 融资保证金比例])

                    if listD:
                        dfD = pd.DataFrame(listD, columns=['证券代码', '证券市场', '融资状态', '融资保证金比例'])
                        self.融资列表 = list(dfD['证券代码'])
                        print(f"成功获取 {len(self.融资列表)} 个融资标的.")
                    else:
                         print("未查询到融资标的或标的数据格式不符。")
                         self.获取本地融资列表() # 尝试从本地加载

                else:
                    print("查询融资标的返回为空。")
                    self.获取本地融资列表() # 尝试从本地加载

            except Exception as e:
                print(f"查询融资标的 API 调用异常: {e}")
                self.获取本地融资列表() # 出错时尝试从本地加载
        else:
            print("非信用账户,跳过查询融资标的。")

        return dfD

    def 获取本地融资列表(self):
        """
        从本地文件 data/dd.txt 加载融资列表作为备用
        文件格式应为 tab 分隔,第一行为表头,包含'证券代码'和'融资状态'列
        """
        pathname = './data/dd.txt'
        print(f"尝试从本地文件 '{pathname}' 加载融资列表...")
        if not os.path.exists(pathname):
             print(f"本地融资列表文件 '{pathname}' 不存在,无法加载。")
             self.融资列表 = []
             return

        try:
            # 使用更健壮的方式读取,处理可能的空行和格式问题
            data = []
            with open(pathname, 'r', encoding='utf-8') as f:
                 header = f.readline().strip().split('\t')
                 for line in f:
                      parts = line.strip().split('\t')
                      if len(parts) == len(header):
                           data.append(parts)

            if not data:
                 print("本地融资列表文件内容为空或格式不正确。")
                 self.融资列表 = []
                 return

            df = pd.DataFrame(data, columns=header)
            # 确保列存在且数据可比较
            if '证券代码' in df.columns and '融资状态' in df.columns:
                 df = df[df['融资状态'] == '正常'] # 筛选状态
                 self.融资列表 = list(df['证券代码'].unique())
                 print(f"成功从本地文件加载 {len(self.融资列表)} 个融资标的。")
            else:
                 print("本地融资列表文件缺少'证券代码'或'融资状态'列。")
                 self.融资列表 = []

        except Exception as e:
            print(f"读取本地融资列表文件 '{pathname}' 时出错: {e}")
            self.融资列表 = []

    def 查询资金(self):
        """
        查询账户资金信息
        :return: (out, df)
                 out: 1 表示成功, 0 表示失败
                 df: pandas DataFrame 包含资金信息 (时间, 净资产, 总资产, 可用资金)
                     对于信用账户,还会包含 融资可用额度, 融资授信额度 等 (作为类的属性更新)
        """
        if not self.connected:
            print("错误: 交易未连接,无法查询资金")
            return 0, pd.DataFrame()

        out = 0
        df = pd.DataFrame()
        current_time = time.strftime("%Y-%m-%d %H:%M:%S")

        try:
            if self.account_type == 'CREDIT':
                asset_list = self.xt_trader.query_credit_detail(self.acc) # 返回列表
                if asset_list:
                    asset = asset_list[0] # 通常只有一个元素
                    总资产 = round(getattr(asset, 'm_dBalance', 0) / 10000, 2) # 使用 getattr 避免属性不存在错误
                    可用资金 = round(getattr(asset, 'm_dAvailable', getattr(asset, 'available', 0)) / 10000, 2) # 兼容不同版本属性名
                    净资产 = round(getattr(asset, 'm_dAssureAsset', 0) / 10000, 2)
                    self.融资可用额度 = round(getattr(asset, 'm_dFinEnableQuota', 0) / 10000, 2)
                    self.融资授信额度 = round(getattr(asset, 'm_dFinMaxQuota', 0) / 10000, 2)
                    print(f"信用账户资金: 净资产={净资产}万, 总资产={总资产}万, 可用={可用资金}万, 融资可用={self.融资可用额度}万")
                    data = [[current_time, 净资产, 总资产, 可用资金]]
                    df = pd.DataFrame(data, columns=['时间', '净资产', '总资产', '可用资金'])
                    out = 1
                else:
                    print("查询信用账户资金返回为空。")

            else: # 普通账户
                asset = self.xt_trader.query_stock_asset(self.acc)
                if asset:
                    总资产 = round(getattr(asset, 'total_asset', 0) / 10000, 2)
                    可用资金 = round(getattr(asset, 'cash', 0) / 10000, 2)
                    净资产 = 总资产 # 普通账户净资产等于总资产
                    print(f"普通账户资金: 净资产/总资产={净资产}万, 可用={可用资金}万")
                    data = [[current_time, 净资产, 总资产, 可用资金]]
                    df = pd.DataFrame(data, columns=['时间', '净资产', '总资产', '可用资金'])
                    out = 1
                else:
                     print("查询普通账户资金返回为空。")

        except Exception as e:
            print(f"查询资金 API 调用异常: {e}")

        return out, df

    def 获取资金(self):
        """查询资金的简单接口,直接返回DataFrame"""
        _, df = self.查询资金()
        return df

    def 查询持仓(self):
        """
        查询账户持仓信息
        :return: (out, df)
                 out: 1 表示成功, 0 表示失败
                 df: pandas DataFrame 包含持仓信息 (时间, 证券代码, 证券数量, 可卖数量, 成本价, 当前价(置0))
        """
        if not self.connected:
            print("错误: 交易未连接,无法查询持仓")
            return 0, pd.DataFrame()

        out = 0
        df = pd.DataFrame()
        current_time = time.strftime("%Y-%m-%d %H:%M:%S")

        try:
            positions = self.xt_trader.query_stock_positions(self.acc)
            if positions is not None: # 查询成功,可能返回空列表 []
                data = []
                for pos in positions:
                    证券代码 = getattr(pos, 'stock_code', '')[:6] # 取代码前6位
                    if not 证券代码: continue # 跳过无效代码

                    证券数量 = int(getattr(pos, 'volume', 0))
                    可卖数量 = int(getattr(pos, 'can_use_volume', 0))
                    成本价 = float(getattr(pos, 'open_price', 0.0))
                    当前价 = 0.0 # API不直接返回当前价, 需自行获取行情

                    # 只记录持仓数量大于0的股票
                    if 证券数量 > 0:
                         datat = [current_time, 证券代码, 证券数量, 可卖数量, 成本价, 当前价]
                         data.append(datat)

                namelist = ['时间', '证券代码', '证券数量', '可卖数量', '成本价', '当前价']
                df = pd.DataFrame(data, columns=namelist)
                # 可以在这里对 DataFrame 进行排序或其他处理
                # df = df.sort_values(by='证券代码')
                out = 1
                print(f"查询到 {len(df)} 条持仓记录。")
            else:
                # 查询调用本身失败(不是返回空列表)
                print("查询持仓 API 调用失败或返回 None。")

        except Exception as e:
            print(f"查询持仓 API 调用异常: {e}")

        return out, df

    def 查询持仓2(self):
        """查询持仓的简单接口,直接返回DataFrame"""
        _, df = self.查询持仓()
        return df

    def _format_order_time(self, timestamp_ms):
        """将毫秒时间戳转换为 HH:MM:SS 格式"""
        try:
            # QMT 的 order_time 有时是秒,有时是毫秒,尝试判断
            if timestamp_ms > time.time() * 10: # 简单判断是否为毫秒
                timestamp_s = timestamp_ms / 1000
            else:
                timestamp_s = timestamp_ms
            return time.strftime("%H:%M:%S", time.localtime(timestamp_s))
        except Exception:
            return "时间错误" # 返回错误提示

    def 查询当日委托(self):
        """
        查询当日所有委托记录
        :return: (out, df)
                 out: 1 表示成功, 0 表示失败
                 df: pandas DataFrame 包含委托信息
                     (委托时间, 证券代码, 证券名称(置'无'), 操作, 委托编号,
                      委托数量, 成交数量, 委托价格, 委托金额, 成交金额)
        """
        if not self.connected:
            print("错误: 交易未连接,无法查询委托")
            return 0, pd.DataFrame()

        # 委托状态映射字典
        status_map = {
            xtconstant.ORDER_UNREPORTED: '未报',
            xtconstant.ORDER_WAIT_REPORTING: '待报',
            xtconstant.ORDER_REPORTED: '已报',
            xtconstant.ORDER_REPORTED_CANCEL: '已报待撤',
            xtconstant.ORDER_PARTSUCC_CANCEL: '部成待撤',
            xtconstant.ORDER_PART_CANCEL: '部撤',
            xtconstant.ORDER_CANCELED: '已撤',
            xtconstant.ORDER_PART_SUCC: '部成',
            xtconstant.ORDER_SUCCEEDED: '已成',
            xtconstant.ORDER_JUNK: '废单',
            xtconstant.ORDER_UNKNOWN: '未知'
        }
        # 操作类型映射
        type_map = {
            xtconstant.STOCK_BUY: '买入',
            xtconstant.STOCK_SELL: '卖出',
            xtconstant.CREDIT_FIN_BUY: '融资买入', # 融资买入
            xtconstant.CREDIT_SLO_SELL: '融券卖出', # 融券卖出
            xtconstant.CREDIT_BUY_SECU_REPAY: '买券还券',
            xtconstant.CREDIT_SELL_SECU_REPAY: '卖券还款',
            xtconstant.CREDIT_DIRECT_SECU_REPAY: '直接还券',
            # 可以根据需要添加更多类型
        }

        out = 0
        df = pd.DataFrame()
        try:
            orders = self.xt_trader.query_stock_orders(self.acc)
            if orders is not None:
                data = []
                for order in orders:
                    证券代码 = getattr(order, 'stock_code', '')[:6]
                    if not 证券代码: continue

                    委托编号 = getattr(order, 'order_id', 0)
                    报单时间戳 = getattr(order, 'order_time', 0) # 可能是秒或毫秒
                    委托时间 = self._format_order_time(报单时间戳)

                    委托类型 = getattr(order, 'order_type', -1)
                    委托方向 = type_map.get(委托类型, f'未知类型({委托类型})')

                    委托数量 = int(getattr(order, 'order_volume', 0))
                    委托价格 = float(getattr(order, 'price', 0.0))
                    成交数量 = int(getattr(order, 'traded_volume', 0))
                    成交均价 = float(getattr(order, 'traded_price', 0.0)) # 注意: QMT的成交均价可能不实时或不准确

                    委托状态码 = getattr(order, 'order_status', xtconstant.ORDER_UNKNOWN)
                    委托状态 = status_map.get(委托状态码, '未知状态')
                    # 委托状态描述 = getattr(order, 'status_msg', '') # 状态描述信息

                    委托金额 = round(委托数量 * 委托价格 / 10000, 2) if 委托价格 > 0 else 0
                    成交金额 = round(成交数量 * 成交均价 / 10000, 2) if 成交均价 > 0 else 0 # 使用成交均价计算

                    操作 = 委托方向 + '' + 委托状态 # 合并方向和状态

                    datat = [委托时间, 证券代码, '无', 操作, 委托编号, 委托数量, 成交数量, 委托价格, 委托金额, 成交金额]
                    data.append(datat)

                namelist = ['委托时间', '证券代码', '证券名称', '操作', '委托编号', '委托数量', '成交数量', '委托价格', '委托金额', '成交金额']
                df = pd.DataFrame(data, columns=namelist)
                # 按时间降序排列
                # df = df.sort_values(by='委托时间', ascending=False)
                out = 1
                print(f"查询到 {len(df)} 条当日委托记录。")
            else:
                print("查询当日委托 API 调用失败或返回 None。")

        except Exception as e:
            print(f"查询当日委托 API 调用异常: {e}")

        return out, df

    def 查询可撤单(self):
        """
        查询当前可以撤销的委托
        :return: (out, df)
                 out: 1 表示成功, 0 表示失败
                 df: pandas DataFrame 包含可撤委托信息
                     (委托时间, 证券代码, 证券名称(置'无'), 操作(仅方向),
                      委托编号, 委托数量, 成交数量, 委托价格)
        """
        if not self.connected:
            print("错误: 交易未连接,无法查询可撤单")
            return 0, pd.DataFrame()

        # 可撤销的状态集合
        cancellable_status = {
            xtconstant.ORDER_UNREPORTED,
            xtconstant.ORDER_WAIT_REPORTING,
            xtconstant.ORDER_REPORTED,
            xtconstant.ORDER_PART_SUCC, # 部成可撤
            xtconstant.ORDER_REPORTED_CANCEL, # 已报待撤理论上也可撤,但可能很快变化
            xtconstant.ORDER_PARTSUCC_CANCEL, # 部成待撤理论上也可撤
        }
        # 操作类型映射
        type_map = {
            xtconstant.STOCK_BUY: '买入',
            xtconstant.STOCK_SELL: '卖出',
            xtconstant.CREDIT_FIN_BUY: '融资买入',
            xtconstant.CREDIT_SLO_SELL: '融券卖出',
            xtconstant.CREDIT_BUY_SECU_REPAY: '买券还券',
            xtconstant.CREDIT_SELL_SECU_REPAY: '卖券还款',
            xtconstant.CREDIT_DIRECT_SECU_REPAY: '直接还券',
        }

        out = 0
        df = pd.DataFrame()
        try:
            orders = self.xt_trader.query_stock_orders(self.acc) # 获取所有当日委托
            if orders is not None:
                data = []
                for order in orders:
                    委托状态码 = getattr(order, 'order_status', xtconstant.ORDER_UNKNOWN)

                    # 判断是否可撤销 (状态在可撤集合中,并且未完全成交)
                    if 委托状态码 in cancellable_status and getattr(order, 'order_volume', 0) > getattr(order, 'traded_volume', 0):
                        证券代码 = getattr(order, 'stock_code', '')[:6]
                        if not 证券代码: continue

                        委托编号 = getattr(order, 'order_id', 0)
                        报单时间戳 = getattr(order, 'order_time', 0)
                        委托时间 = self._format_order_time(报单时间戳)

                        委托类型 = getattr(order, 'order_type', -1)
                        操作 = type_map.get(委托类型, f'未知({委托类型})') # 只显示方向

                        委托数量 = int(getattr(order, 'order_volume', 0))
                        成交数量 = int(getattr(order, 'traded_volume', 0))
                        委托价格 = float(getattr(order, 'price', 0.0))

                        datat = [委托时间, 证券代码, '无', 操作, 委托编号, 委托数量, 成交数量, 委托价格]
                        data.append(datat)

                namelist = ['委托时间', '证券代码', '证券名称', '操作', '委托编号', '委托数量', '成交数量', '委托价格']
                df = pd.DataFrame(data, columns=namelist)
                # df = df.sort_values(by='委托时间', ascending=False)
                out = 1
                print(f"查询到 {len(df)} 条可撤委托记录。")
            else:
                print("查询可撤单 (获取当日委托) API 调用失败或返回 None。")

        except Exception as e:
            print(f"查询可撤单 API 调用异常: {e}")

        return out, df

    def 委托(self, 方向, 股票代码, 委托价格, 委托数量):
        """
        执行委托下单

        :param 方向: int
                     0: 买入 (STOCK_BUY)
                     1: 卖出 (STOCK_SELL)
                     2: 融资买入 (CREDIT_FIN_BUY) (仅信用账户有效)
                     11: 全部卖出 (根据持仓自动计算数量)
                     (其他类型如融券卖出等可按需添加)
        :param 股票代码: str, 6位代码 (例如 '600036')
        :param 委托价格: float, 委托的价格
        :param 委托数量: int, 委托的数量 (对于方向11,此参数将被忽略)
        :return: order_id (int): 委托成功时返回委托ID (非0), 失败或参数无效时返回 0
        """
        if not self.connected:
            print("错误: 交易未连接,无法执行委托")
            return 0

        股票代码 = str(股票代码)[:6] # 确保是6位字符串
        方向 = int(方向)
        委托数量 = int(委托数量)
        委托价格 = round(float(委托价格), 2) # 保留两位小数

        # --- 参数校验 ---
        if not (股票代码.isdigit() and len(股票代码) == 6):
             print(f"委托失败: 无效的股票代码 '{股票代码}'")
             return 0
        if 委托价格 <= 0:
             print(f"委托失败: 无效的委托价格 {委托价格}")
             return 0
        # 方向11 特殊处理数量
        if 方向 != 11 and 委托数量 <= 0:
             print(f"委托失败: 委托数量必须大于 0 (收到 {委托数量})")
             return 0

        # --- 构造 QMT 股票代码 ---
        if 股票代码.startswith('6') or 股票代码.startswith('5') or 股票代码.startswith('11'): # 沪市 A股, 基金, 转债等
            股票代码_qmt = f"{股票代码}.SH"
        elif 股票代码.startswith('0') or 股票代码.startswith('3') or 股票代码.startswith('1') or 股票代码.startswith('13'): # 深市 A股, 创业板, 基金, 国债逆回购等
            股票代码_qmt = f"{股票代码}.SZ"
        else:
            print(f"委托失败: 未知的股票代码前缀 '{股票代码[:1]}', 无法确定市场")
            return 0

        # --- 确定委托类型和数量 ---
        order_type = -1
        final_委托数量 = 委托数量
        log_prefix = "" # 日志前缀

        if 方向 == 0: # 普通买入
            order_type = xtconstant.STOCK_BUY
            log_prefix = "普通买入"
            # 可选:增加资金校验逻辑
            # _, df_asset = self.查询资金()
            # if not df_asset.empty:
            #     可用资金 = df_asset['可用资金'].iloc[0] * 10000
            #     所需资金 = 委托价格 * final_委托数量
            #     if 所需资金 > 可用资金:
            #         print(f"委托失败: {log_prefix} {股票代码} 资金不足 (需要 {所需资金:.2f}, 可用 {可用资金:.2f})")
            #         return 0
            # else:
            #     print("警告: 无法获取资金信息进行买入校验")

        elif 方向 == 1: # 普通卖出
            order_type = xtconstant.STOCK_SELL
            log_prefix = "普通卖出"
            # 可选:增加可卖数量校验逻辑
            _, df_pos = self.查询持仓()
            pos_info = df_pos[df_pos['证券代码'] == 股票代码]
            if not pos_info.empty:
                可卖数量 = pos_info['可卖数量'].iloc[0]
                if final_委托数量 > 可卖数量:
                    print(f"委托失败: {log_prefix} {股票代码} 可卖数量不足 (需要 {final_委托数量}, 可卖 {可卖数量})")
                    return 0
            else:
                print(f"委托失败: {log_prefix} {股票代码} 无持仓信息")
                return 0

        elif 方向 == 11: # 全部卖出
            order_type = xtconstant.STOCK_SELL
            log_prefix = "全部卖出"
            _, df_pos = self.查询持仓()
            pos_info = df_pos[df_pos['证券代码'] == 股票代码]
            if not pos_info.empty:
                final_委托数量 = pos_info['可卖数量'].iloc[0]
                if final_委托数量 <= 0:
                     print(f"委托失败: {log_prefix} {股票代码} 可卖数量为 0")
                     return 0
                print(f"全部卖出 {股票代码}, 计算可卖数量为: {final_委托数量}")
            else:
                print(f"委托失败: {log_prefix} {股票代码} 无持仓信息")
                return 0

        elif 方向 == 2: # 融资买入
            if self.account_type != 'CREDIT':
                print("委托失败: 融资买入仅支持信用账户")
                return 0
            # 检查是否为融资标的 (如果列表非空)
            if self.融资列表 and 股票代码 not in self.融资列表:
                print(f"委托失败: {股票代码} 不是融资标的,无法进行融资买入 (将尝试普通买入)")
                # 可以选择在这里转为普通买入 (方向=0) 或直接失败
                # return self.委托(0, 股票代码, 委托价格, 委托数量) # 转为普通买入
                # 这里选择直接失败,让调用者决定
                return 0
            order_type = xtconstant.CREDIT_FIN_BUY
            log_prefix = "融资买入"
            # 可选:增加融资额度校验
            # _, df_asset = self.查询资金() # 会更新 self.融资可用额度
            # if hasattr(self, '融资可用额度'):
            #     所需额度 = 委托价格 * final_委托数量 # 简化估算,未考虑保证金比例
            #     if 所需额度 > self.融资可用额度 * 10000:
            #          print(f"委托失败: {log_prefix} {股票代码} 融资可用额度不足 (需要约 {所需额度:.2f}, 可用 {self.融资可用额度*10000:.2f})")
            #          return 0
            # else:
            #     print("警告: 无法获取融资额度信息进行校验")

        else:
            print(f"委托失败: 不支持的委托方向代码 {方向}")
            return 0

        # --- 执行委托 ---
        if final_委托数量 <= 0:
             print(f"委托失败: 最终计算出的委托数量为 0 或负数 ({final_委托数量})")
             return 0

        print(f"准备委托: {log_prefix} {股票代码_qmt}, 数量: {final_委托数量}, 价格: {委托价格}")
        try:
            # 使用限价委托 (FIX_PRICE)
            # strategy_name 和 remark 为可选参数
            order_id = self.xt_trader.order_stock(
                self.acc,
                stock_code=股票代码_qmt,
                order_type=order_type,
                order_volume=int(final_委托数量), # 确保是整数
                price_type=xtconstant.FIX_PRICE, # 限价
                price=委托价格,
                strategy_name='strategy_default',
                remark='api_order'
            )

            if order_id > 0:
                print(f"委托请求已发送: {log_prefix} {股票代码_qmt}, 数量: {final_委托数量}, 价格: {委托价格}, 返回委托ID: {order_id}")
                # 注意:返回 order_id 不代表委托一定成功或被交易所接受,需要通过回调或查询确认最终状态
                return order_id
            else:
                # order_stock 返回 0 或负数表示发送失败
                print(f"!!! 委托发送失败: {log_prefix} {股票代码_qmt}, order_stock 返回 {order_id}")
                # 尝试获取最近的错误信息 (如果回调有记录的话)
                # if self.callback and hasattr(self.callback, 'last_error'):
                #     print(f"    最近错误: {self.callback.last_error}")
                return 0

        except Exception as e:
            print(f"!!! 委托 API 调用异常: {log_prefix} {股票代码_qmt}, 错误: {e}")
            return 0

    def 撤单(self, order_id):
        """
        根据委托编号撤销委托

        :param order_id: int, 需要撤销的委托编号 (由 self.委托 或 查询委托/可撤单 获得)
        :return: cancel_result (int): 撤单指令发送结果,0表示成功发送,负数表示失败。
                 注意:发送成功不代表撤单一定成功,最终状态需查询确认。
        """
        if not self.connected:
            print("错误: 交易未连接,无法执行撤单")
            return -1 # 返回错误码

        if not isinstance(order_id, int) or order_id <= 0:
            print(f"撤单失败: 无效的委托编号 {order_id}")
            return -1

        print(f"准备撤单: 委托编号 {order_id}")
        try:
            # QMT 使用 order_id 进行撤单
            cancel_result = self.xt_trader.cancel_order_stock(self.acc, int(order_id))

            if cancel_result == 0:
                print(f"撤单指令已发送: 委托编号 {order_id}")
                # 实际撤单是否成功需要后续通过查询委托状态确认
            else:
                # 返回负数表示发送失败
                print(f"!!! 撤单指令发送失败: 委托编号 {order_id}, cancel_order_stock 返回 {cancel_result}")

            return cancel_result

        except Exception as e:
            print(f"!!! 撤单 API 调用异常: 委托编号 {order_id}, 错误: {e}")
            return -1 # 返回错误码

    def stop(self):
        """停止交易并断开连接"""
        if self.xt_trader:
            print("停止交易线程并断开连接...")
            self.xt_trader.stop()
            self.connected = False
            print("交易已停止。")

# --- 主程序入口 ---
if __name__ == '__main__':
    # --- 1. 创建交易实例 ---
    trader = TradeApiQmt()

    # --- 2. 设置账户信息 (!!!请根据实际情况修改!!!) ---
    # 示例 1: 中信证券模拟信用账户
    qmt_path = 'D:\\rj\\中信证券QMT交易终端仿真\\userdata_mini' # QMT 安装路径下的 userdata_mini
    account_id = '8009170040' # 你的资金账号
    account_name = '账户迅投模拟信用' # 自定义名称,包含'信用'字样

    # 示例 2: 中信证券普通账户 (假设路径和账号)
    # qmt_path = 'C:\\Program Files\\中信证券QMT\\userdata_mini'
    # account_id = '101300000618'
    # account_name = '账户中信普通'

    trader.set_account(qmt_path, account_id, account_name)

    # --- 3. 初始化并连接 ---
    if trader.初始化():
        print("\n--- 初始化成功,开始执行操作 ---")

        # --- 4. 查询操作示例 ---
        print("\n--- 查询资金 ---")
        out_asset, df_asset = trader.查询资金()
        if out_asset:
            print(df_asset.to_string(index=False))

        time.sleep(1) # 等待一下,避免过于频繁的API调用

        print("\n--- 查询持仓 ---")
        out_pos, df_pos = trader.查询持仓()
        if out_pos:
            if not df_pos.empty:
                print(df_pos.to_string(index=False))
            else:
                print("当前无持仓。")

        time.sleep(1)

        print("\n--- 查询当日委托 ---")
        out_orders, df_orders = trader.查询当日委托()
        if out_orders:
            if not df_orders.empty:
                print(df_orders.to_string(index=False))
            else:
                print("当日无委托记录。")

        time.sleep(1)

        print("\n--- 查询可撤单 ---")
        out_cancelable, df_cancelable = trader.查询可撤单()
        if out_cancelable:
            if not df_cancelable.empty:
                print(df_cancelable.to_string(index=False))
                # --- 5. 撤单示例 (撤销第一条可撤委托) ---
                # first_order_id_to_cancel = df_cancelable['委托编号'].iloc[0]
                # print(f"\n--- 尝试撤销委托: {first_order_id_to_cancel} ---")
                # cancel_result = trader.撤单(first_order_id_to_cancel)
                # print(f"撤单指令发送结果: {cancel_result} (0表示成功发送)")
            else:
                print("当前无可撤委托。")

        # --- 6. 下单示例 (!!!请谨慎操作!!!) ---
        # print("\n--- 委托下单示例 (买入中国平安 100 股,价格 45.00) ---")
        # order_id_buy = trader.委托(方向=0, 股票代码='601318', 委托价格=45.00, 委托数量=100)
        # if order_id_buy > 0:
        #     print(f"买入委托已发送,委托ID: {order_id_buy}")
        #     # 可以尝试撤销这个刚下的单
        #     # time.sleep(2) # 等待委托回报
        #     # print(f"\n--- 尝试撤销刚才的买入委托: {order_id_buy} ---")
        #     # trader.撤单(order_id_buy)

        # print("\n--- 委托下单示例 (融资买入 贵州茅台 100 股,价格 1700.00) ---")
        # # 确保账户是信用账户且标的是融资标的
        # if trader.account_type == 'CREDIT' and ('600519' in trader.融资列表 or not trader.融资列表): # 如果融资列表为空则也尝试
        #     order_id_fin_buy = trader.委托(方向=2, 股票代码='600519', 委托价格=1700.00, 委托数量=100)
        #     if order_id_fin_buy > 0:
        #         print(f"融资买入委托已发送,委托ID: {order_id_fin_buy}")
        # else:
        #      print("跳过融资买入示例 (非信用账户或非融资标的)")

        # print("\n--- 委托下单示例 (卖出 中国平安 100 股,价格 46.00) ---")
        # # 确保有持仓且可卖
        # if not df_pos[df_pos['证券代码'] == '601318'].empty and df_pos[df_pos['证券代码'] == '601318']['可卖数量'].iloc[0] >= 100:
        #     order_id_sell = trader.委托(方向=1, 股票代码='601318', 委托价格=46.00, 委托数量=100)
        #     if order_id_sell > 0:
        #         print(f"卖出委托已发送,委托ID: {order_id_sell}")
        # else:
        #      print("跳过卖出示例 (无足够持仓或可卖数量)")

        # print("\n--- 委托下单示例 (全部卖出 中国平安,价格 46.00) ---")
        # # 确保有持仓且可卖
        # if not df_pos[df_pos['证券代码'] == '601318'].empty and df_pos[df_pos['证券代码'] == '601318']['可卖数量'].iloc[0] > 0:
        #     order_id_sell_all = trader.委托(方向=11, 股票代码='601318', 委托价格=46.00, 委托数量=0) # 数量参数在方向11时无效
        #     if order_id_sell_all > 0:
        #         print(f"全部卖出委托已发送,委托ID: {order_id_sell_all}")
        # else:
        #      print("跳过全部卖出示例 (无持仓或可卖数量)")


        # --- 7. 结束时停止 ---
        print("\n--- 操作完成 ---")
        # trader.stop() # 可以在这里停止,或者在程序退出时自动处理

    else:
        print("\n--- 初始化失败,无法执行后续操作 ---")

    # 脚本结束前确保停止
    if trader.connected:
         trader.stop()

    print("\n--- 程序结束 ---")

代码解释:

  1. MyXtQuantTraderCallback:

    • 继承自 XtQuantTraderCallback,用于处理 QMT 推送的事件。
    • on_disconnected: 当与交易服务器的连接断开时被调用。
    • on_stock_order: 当有新的委托状态更新(如下单成功、部分成交、完全成交、已撤销等)时被调用。参数 orderXtOrder 对象,包含详细的委托信息。
    • on_order_error: 当委托遇到错误(如资金不足、涨跌停限制、废单等)时被调用。参数 order_errorXtOrderError 对象,包含错误详情。
  2. TradeApiQmt:

    • __init__: 初始化实例变量,包括 QMT 路径、账户号、账户名、会话 ID、融资列表、连接状态等。
    • set_account: 用于设置 QMT 的 userdata_mini 路径、交易账户号码和自定义的账户名称。这是调用 初始化 前必须执行的步骤。
    • 初始化:
      • 检查账户信息是否设置。
      • 创建 XtQuantTrader 核心交易对象。
      • 根据 account_name 是否包含 “信用” 或 “credit” 来创建 StockAccount 对象,区分普通账户和信用账户。
      • 创建并注册上面定义的 MyXtQuantTraderCallback 回调处理类。
      • 调用 xt_trader.start() 启动内部交易处理线程。
      • 调用 xt_trader.connect() 连接交易服务器,包含重试逻辑。
      • 连接成功后,如果是信用账户,调用 查询融资融券标的
      • 返回连接成功与否的布尔值。
    • 查询融资融券标的:
      • 仅当账户类型是 ‘CREDIT’ 时执行。
      • 调用 xt_trader.query_credit_subjects() 获取融资融券标的信息。
      • 解析返回结果,提取 A 股代码,存入 self.融资列表,并返回 DataFrame。
      • 包含错误处理和调用 获取本地融资列表 作为备用。
    • 获取本地融资列表:
      • 作为 查询融资融券标的 失败时的备用方案。
      • 尝试从 ./data/dd.txt 文件读取融资列表。需要该文件存在且格式正确(tab 分隔,有表头)。
      • 包含文件不存在和读取错误的异常处理。
    • 查询资金:
      • 根据账户类型 (STOCKCREDIT) 调用不同的 QMT API (query_stock_assetquery_credit_detail)。
      • 解析返回的资金信息(总资产、可用资金、净资产等),注意单位是万元。
      • 对于信用账户,还会更新 self.融资可用额度self.融资授信额度 属性。
      • 返回查询结果状态 (1/0) 和包含资金信息的 DataFrame。
    • 获取资金: 查询资金 的简化版本,仅返回 DataFrame。
    • 查询持仓:
      • 调用 xt_trader.query_stock_positions() 获取当前持仓。
      • 遍历持仓列表,提取所需信息(代码、数量、可卖数量、成本价)。注意 API 不返回实时当前价。
      • 只记录持仓数量大于 0 的记录。
      • 返回查询结果状态 (1/0) 和包含持仓信息的 DataFrame。
    • 查询持仓2: 查询持仓 的简化版本,仅返回 DataFrame。
    • _format_order_time: 辅助函数,将 QMT 返回的可能为秒或毫秒的时间戳统一格式化为 HH:MM:SS
    • 查询当日委托:
      • 调用 xt_trader.query_stock_orders() 获取当日所有委托记录。
      • 使用字典 (status_map, type_map) 将 QMT 返回的状态码和类型码转换为可读的中文描述。
      • 解析每条委托信息,计算委托金额和成交金额(万元)。
      • 返回查询结果状态 (1/0) 和包含详细委托信息的 DataFrame。
    • 查询可撤单:
      • 调用 xt_trader.query_stock_orders() 获取所有当日委托。
      • 定义一个包含可撤销状态码的集合 cancellable_status
      • 遍历所有委托,筛选出状态在 cancellable_status 中且委托数量大于成交数量的委托。
      • 返回查询结果状态 (1/0) 和包含可撤委托信息的 DataFrame。
    • 委托:
      • 核心下单函数。
      • 参数校验:检查代码、价格、数量(方向 11 除外)。
      • 根据股票代码前缀判断市场(.SH.SZ)。
      • 根据 方向 参数确定 QMT 的 order_type
        • 0: xtconstant.STOCK_BUY (买入)
        • 1: xtconstant.STOCK_SELL (卖出)
        • 2: xtconstant.CREDIT_FIN_BUY (融资买入),会检查账户类型和融资标的。
        • 11: xtconstant.STOCK_SELL (全部卖出),会自动查询可卖数量。
      • (可选)注释掉了资金、可卖数量、融资额度的预校验逻辑,因为 QMT 服务端会进行校验,并且实时查询可能影响性能。
      • 调用 xt_trader.order_stock() 发送限价 (FIX_PRICE) 委托指令。
      • 返回 QMT 返回的 order_id。成功发送指令时 order_id > 0,失败则为 0 或负数。
    • 撤单:
      • 根据传入的 order_id 撤销委托。
      • 参数校验:检查 order_id 是否有效。
      • 调用 xt_trader.cancel_order_stock() 发送撤单指令。
      • 返回 QMT 的撤单指令发送结果(0 表示成功发送,负数表示失败)。
    • stop:
      • 调用 xt_trader.stop() 停止交易线程并断开连接。
  3. if __name__ == '__main__': (主程序块):

    • 演示如何使用 TradeApiQmt 类。
    • 创建 TradeApiQmt 实例。
    • 重要: 调用 trader.set_account() 设置你的 QMT 路径、账户号、账户名。
    • 调用 trader.初始化() 连接服务器。
    • 如果初始化成功,则按顺序演示调用 查询资金查询持仓查询当日委托查询可撤单
    • 注释掉了下单 (委托) 和撤单 (撤单) 的示例代码,使用前请务必理解风险并取消注释
    • 最后调用 trader.stop() (或依赖程序结束时的自动清理)。

如何使用:

  1. 安装 xtquant: 如果没有安装,打开命令行/终端,运行 pip install xtquant
  2. 修改账户信息: 在 if __name__ == '__main__': 块中,找到 trader.set_account() 这一行,将其参数修改为你自己的 QMT userdata_mini 路径、资金账号和自定义账户名(确保信用账户名包含"信用"或"credit")。
  3. 运行代码: 直接运行此 Python 脚本 (python your_script_name.py)。
  4. 查看输出: 控制台会打印连接状态、查询结果等信息。
  5. 下单/撤单 (可选): 如果需要测试下单或撤单,请取消 if __name__ == '__main__': 块中相关示例代码的注释,并确保参数正确。实盘操作请务必谨慎!
  6. 融资融券标的备用文件 (可选): 如果你需要使用本地文件作为融资融券标的的备用列表,请在脚本同目录下创建一个 data 文件夹,并在其中创建一个名为 dd.txt 的文件。文件内容应为 Tab 分隔,第一行是表头(必须包含 证券代码融资状态 列),后续每行是一条标的信息,例如:
    证券代码	证券市场	融资状态	融资保证金比例
    600036	SH	正常	0.7
    000001	SZ	正常	0.7
    ...
    
QMT(Quantitative Multi-thread Trading)是国泰君安推出的量化交易平台,支持编写程序化交易策略。MACD金叉是一种常见的技术指标信号,表示短期均线向上穿越长期均线,通常被视为买入信号。 下面是一个基于QMT平台实现MACD金叉实盘交易的基本代码框架: ```python # 定义策略类并初始化参数 class MACDJinchaStrategy: def __init__(self): self.fast_period = 12 # 快速线周期 self.slow_period = 26 # 慢速线周期 self.signal_period = 9 # 信号线周期 def on_bar(self, bar_data): # 计算MACD值 macd, signal, hist = talib.MACD(bar_data['close'], fastperiod=self.fast_period, slowperiod=self.slow_period, signalperiod=self.signal_period) # 判断是否发生金叉 if len(signal) > 1 and macd[-1] > signal[-1] and macd[-2] <= signal[-2]: print("MACD 发生金叉") # 执行买入操作 (需替换为您账户的实际下单函数) order_buy(symbol="目标股票", quantity=100) elif len(signal) > 1 and macd[-1] < signal[-1] and macd[-2] >= signal[-2]: print("MACD 发生死叉") # 执行卖出操作 (需替换为您账户的实际平仓函数) order_sell(symbol="目标股票", quantity=100) # 实际运行部分 if __name__ == '__main__': strategy_instance = MACDJinchaStrategy() # 假设bar_data是从行情推送获取的数据 while True: latest_bar = get_latest_market_data() # 获取最新K线数据 strategy_instance.on_bar(latest_bar) ``` 注意:以上代码仅为示例结构,在实际部署之前需要调整适配您的环境、证券品种以及风险控制需求等细节内容,并经过充分测试后再投入生产!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值