掘金终端实现仿真交易

穷人版仿真交易解决方案

掘金仿真交易终端可以提供模拟精准撮合成交。并且提供了python的工具包gmtrade以供用户与交易终端交互。相当于前文的CTPgmtrade代替。

VNPY对掘金仿真交易的封装在vnpy_gm包中。

代码解读

掘金交易网关GmGateway

package

from gmtrade.api import (
    set_token,
    order_volume,
    order_cancel,
    get_cash,
    get_positions,
    get_orders,
    get_execution_reports,
    login,
    set_endpoint,
    account
)

这里从掘金终端提供的API接口中,导入了一系列与交易有关的函数

  • order_volume,order_cancel:按指定量委托,取消委托
  • get_cash,get_positions,get_orders,get_execution_reports:查询现金余额、持仓、订单、执行反馈
from gmtrade.api.storage import ctx
from gmtrade.api.callback import callback_controller
from gmtrade.csdk.c_sdk import (
    c_status_fail,
    py_gmi_set_data_callback,
    py_gmi_start,
    py_gmi_stop
)

GmGateway的实现

class GmGateway(BaseGateway):
    """
    VeighNa用于对接掘金量化终端的交易接口。
    """

    default_name: str = "GM"

    default_setting: Dict[str, str] = {
        "Token": "",
        "账户ID": ""
    }

    exchanges: List[str] = list(EXCHANGE_GM2VT.values())

    def __init__(self, event_engine: EventEngine, gateway_name: str) -> None:
        """构造函数"""
        super().__init__(event_engine, gateway_name)

        self.md_api: "GmMdApi" = GmMdApi(self)
        self.td_api: "GmTdApi" = GmTdApi(self)

        self.run_timer: Thread = Thread(target=self.process_timer_event)

    def connect(self, setting: dict) -> None:
        """连接交易接口"""
        token: str = setting["Token"]
        accountid: str = setting["账户ID"]

        self.md_api.init()
        self.td_api.connect(token, accountid)
        self.init_query()

    def subscribe(self, req: SubscribeRequest) -> None:
        """订阅行情"""
        self.md_api.subscribe(req)

    def send_order(self, req: OrderRequest) -> str:
        """委托下单"""
        return self.td_api.send_order(req)

    def cancel_order(self, req: CancelRequest) -> None:
        """委托撤单"""
        self.td_api.cancel_order(req)

    def query_account(self) -> None:
        """查询资金"""
        self.td_api.query_account()

    def query_position(self) -> None:
        """查询持仓"""
        self.td_api.query_position()

    def process_timer_event(self) -> None:
        """定时事件处理"""
        while self.td_api._active and self.md_api._active:
            sleep(3)
            self.query_position()
            self.query_account()
            self.md_api.query_realtime_quotes()

    def init_query(self) -> None:
        """初始化查询任务"""
        self.run_timer.start()

    def close(self) -> None:
        """关闭接口"""
        self.md_api.close()
        self.td_api.close()

前面的文章说到,在BaseGateway中,已经定义好了与VNPY事件引擎交互的函数,所以在继承模板类之后,没有再定义这类函数。
代码主要是具体定义与网关交互的函数。与网关交互的函数又与每个网关需要用到的行情接口与交易接口相关。故对于每一种交易网关,都需要定义其独特的行情接口与交易接口类。

在CTP中,行情接口与交易接口分别是MdApiTdApi。此处沿用了这一命名方式。但与CTP既支持实时行情,又支持交易不同,使用掘金终端进行仿真交易时,实时行情来自于Tushare。故实施方案为:

  • 行情接口:GmMdApi封装Tushare
  • 交易接口:GmTdApi封装gmtrade

GmGateway的初始化中,可以看到一开始就调用了这两个接口:

    self.md_api: "GmMdApi" = GmMdApi(self)
    self.td_api: "GmTdApi" = GmTdApi(self)

后续与网关交互的函数,都是对md_api或是td_api的函数做进一步封装。

实时行情的获取

重点看process_timer_event:其逻辑为只要md_apitd_api是运行中的,就会不断地每隔3秒调用md_api.query_realtime_quotes()拿到一个快照数据。

    def process_timer_event(self) -> None:
    """定时事件处理"""
        while self.td_api._active and self.md_api._active:
            sleep(3)
            self.query_position()
            self.query_account()
            self.md_api.query_realtime_quotes()

在初始化中,可以看到有一个专门的线程来执行获取实时行情:

    self.run_timer: Thread = Thread(target=self.process_timer_event)

行情接口

行情接口封装了Tushare的获取实时行情、历史行情等功能(此处略去历史行情)。

class GmMdApi:

    def __init__(self, gateway: GmGateway) -> None:
        """构造函数"""
        self.gateway: GmGateway = gateway
        self.gateway_name: str = gateway.gateway_name

        self.username: str = SETTINGS["datafeed.username"]
        self.password: str = SETTINGS["datafeed.password"]

        self._active: bool = False
        self.subscribed: set = set()

    def subscribe(self, req: SubscribeRequest) -> None:
        """订阅行情"""
        if req.symbol in symbol_contract_map:
            self.subscribed.add(req.symbol)

关注订阅集合subscribed,当要订阅合约时,调用GmGateway.subscribe -> GmMdApi.subscribe,将一个合约放到subscribed中。
可以看到,这里订阅合约只是在本地储存一个集合,而并不是上传至网关获取权限。

    def init(self) -> None:
        """初始化"""
        ts.set_token(self.password)

        self.pro: DataApi = ts.pro_api()
        self._active = True
        self.gateway.write_log("数据服务初始化完成")

以上传入了Tushare的Key,对Tushare的接口做了初始化。

    def query_realtime_quotes(self) -> None:
        """查询行情数据"""
        try:
            df: DataFrame = ts.get_realtime_quotes(self.subscribed)
        except IOError:
            return

        if df is not None:
            # 处理原始数据中的NaN值
            df.fillna(0, inplace=True)

            for ix, row in df.iterrows():
                dt: str = row["date"].replace("-", "") + " " + row["time"].replace(":", "")
                contract: ContractData = symbol_contract_map[row["code"]]

                tick: tick = TickData(......)
                self.gateway.on_tick(tick)

query_realtime_quotes调用Tushare接口获取了pd.DataFrame格式的快照数据。
此处将df重新整理成VNPY的TickData格式,再调用gateway.on_tick(tick),将Tick数据推送至事件引擎。

至此,数据链条就完整了:

实时行情获取 -> 推送至事件引擎 -> CTA引擎处理事件 -> CTA策略接收到实时行情 -> 推送到K线合成器、K线池 -> 策略生成交易信号

    def close(self) -> None:
        """关闭连接"""
        if self._active:
            self._active = False

最后,提供关闭行情接口的函数。

交易接口

交易接口封装了gmtrade关于交易的接口。

class GmTdApi:

    def __init__(self, gateway: GmGateway):
        """构造函数"""
        super().__init__()

        self.gateway: GmGateway = gateway
        self.gateway_name: str = gateway.gateway_name

        self.inited: bool = False
        self._active: bool = False

    def onconnected(self) -> None:
        """服务器连接成功回报"""
        self.gateway.write_log("交易服务器连接成功")

    def ondisconnected(self) -> None:
        """服务器连接断开回报"""
        self.gateway.write_log("交易服务器连接断开")

    def onRtnOrder(self, order) -> None:
        """生成时间戳"""
        type: OrderType = ORDERTYPE_GM2VT.get(order.order_type, None)
        if type is None:
            return

        exchange, symbol = order.symbol.split(".")
        order_data: OrderData = OrderData(......)
        self.gateway.on_order(order_data)

        if order.ord_rej_reason_detail:
            self.gateway.write_log(f"委托拒单:{order.ord_rej_reason_detail}")

    def onRtnTrade(self, rpt) -> None:
        """生成时间戳"""
        if rpt.exec_type != 15:
            if rpt.ord_rej_reason_detail:
                self.gateway.write_log(rpt.ord_rej_reason_detail)
            return

        exchange, symbol = rpt.symbol.split(".")
        trade: TradeData = TradeData(......)
        self.gateway.on_trade(trade)

    def on_error(self, code, info) -> None:
        """输出错误信息"""
        self.gateway.write_log(f"错误代码:{code},信息:{info}")

    def connect(self, token: str, accountid: str) -> None:
        """连接交易接口"""
        if not self.inited:
            self.inited = True

            set_token(token)
            set_endpoint()
            login(account(accountid))
            err: int = self.init_callback()
            if err:
                self.gateway.write_log(f"交易服务器登陆失败,错误码{err}")
                return

            self.query_order()
            self.query_trade()

        else:
            self.gateway.write_log("已经初始化,请勿重复操作")

    def init_callback(self) -> int:
        """注册回调"""
        ctx.inside_file_module = self

        ctx.on_execution_report_fun = self.onRtnTrade
        ctx.on_order_status_fun = self.onRtnOrder
        ctx.on_trade_data_connected_fun = self.onconnected
        ctx.on_trade_data_disconnected_fun = self.ondisconnected

        ctx.on_error_fun = self.on_error

        py_gmi_set_data_callback(callback_controller)  # 设置事件处理的回调函数

        status: int = py_gmi_start()  # type: int
        if c_status_fail(status, 'gmi_start'):
            self._active = False
            return status
        else:
            self._active = True
        return status

以上均为回调函数,即掘金终端作为仿真柜台,会时刻输出状态变化的反馈,如:订单状态、成交信息等。
ctx是提供给API的唯一上下文实例,仿真柜台的各种反馈消息都通过ctx传递。
所以这里是定义了一系列函数,从ctx这里接受信息,进而与仿真柜台对接。

    def send_order(self, req: OrderRequest) -> str:
        """委托下单"""
        if req.offset not in POSITIONEFFECT_VT2GM:
            self.gateway.write_log("请选择开平方向")
            return ""

        type: int = ORDERTYPE_VT2GM.get(req.type, None)
        if type is None:
            self.gateway.write_log(f"不支持的委托类型: {req.type.value}")
            return ""

        exchange: str = EXCHANGE_VT2GM.get(req.exchange, None)

        if exchange is None:
            self.gateway.write_log(f"不支持的交易所: {req.exchange.value}")
            return ""

        symbol: str = exchange + "." + req.symbol

        order_data: list = order_volume(
            symbol=symbol,
            volume=int(req.volume),
            side=DIRECTION_VT2GM[req.direction],
            order_type=type,
            price=req.price,
            position_effect=POSITIONEFFECT_VT2GM[req.offset],
        )
        orderid: str = order_data[0].cl_ord_id

        order: OrderData = req.create_order_data(orderid, self.gateway_name)
        self.gateway.on_order(order)

        return order.vt_orderid

    def cancel_order(self, req: CancelRequest) -> None:
        """委托撤单"""
        cancel_order: dict = {"cl_ord_id": req.orderid}

        order_cancel(wait_cancel_orders=cancel_order)

以上是订单的提交与撤单函数

  • send_order中,调用order_volume即是将下单请求报给掘金终端。然后,掘金终端会返回下单请求对应的订单数据order_data,经整理后,由gateway.on_order(order)推送至本地的订单管理系统。
    • 由于网关的订单组织形式与VNPY默认形式也许不同(如:交易所代码,证券代码形式等),才需要做各种各样的映射。
    • 订单信息要推送至本地的订单管理系统,才能在UI界面上看到。
  • cancel_order中,调用order_cancel即是将撤单请求报给掘金终端。
    def query_position(self) -> None:
        """查询持仓"""
        data: list = get_positions()

        for d in data:
            exchange_, symbol = d.symbol.split(".")
            exchange: Exchange = EXCHANGE_GM2VT.get(exchange_, None)
            if not exchange:
                continue

            position: PositionData = PositionData(......)
            self.gateway.on_position(position)

    def query_account(self) -> None:
        """查询账户资金"""
        data = get_cash()
        if not data:
            self.gateway.write_log("请检查accountid")
            return

        account: AccountData = AccountData(......)
        account.available = round(data.available, 2)
        self.gateway.on_account(account)

    def query_order(self) -> None:
        """查询委托信息"""
        data: list = get_orders()

        for d in data:
            type: OrderType = ORDERTYPE_GM2VT.get(d.order_type, None)
            exchange_, symbol = d.symbol.split(".")
            exchange: Exchange = EXCHANGE_GM2VT.get(exchange_, None)

            if not type or not exchange:
                continue

            order: OrderData = OrderData(......)
            self.gateway.on_order(order)

        self.gateway.write_log("委托信息查询成功")

    def query_trade(self) -> None:
        """查询成交信息"""
        data: list = get_execution_reports()

        for d in data:
            exchange_, symbol = d.symbol.split(".")
            exchange: Exchange = EXCHANGE_GM2VT.get(exchange_, None)
            if not exchange:
                continue

            trade: TradeData = TradeData(......)
            self.gateway.on_trade(trade)

        self.gateway.write_log("成交信息查询成功")

    def close(self) -> None:
        """关闭连接"""
        if self.inited:
            self._active = False
            py_gmi_stop()

以上均为查询函数,每一种查询都需要调用相应的回调函数,将查询信息推送至VNPY事件引擎,再交由事件引擎处理。目的是将交易信息实时显示在本地VNPY界面上。

posted @   superzzh  阅读(15)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示