VeighNa量化社区
你的开源社区量化交易平台
hxxjava's Avatar
Member
离线
419 帖子
声望: 155

1. 升级原因:

由于vnpy系统升级之最新的3.0版本,python底层的对象继承机制发生变化,导致原来的一部分绘图部件因为多继承而发生初始化失败,无法使用,必须升级。
近期不少vnpy的会员朋友不断地私信我,反映这些绘图部件用不了了,因为本人最近忙于交易策略的开发,无暇顾及,实在是抽不出时间,请大家谅解!
现在问题已经解决,可以放心使用。

2. 升级代码:

2.1 修改BarManager

修改vnpy\chart\manager.py中的BarManager,为它添加一个函数:

    def get_bar_idx(self,trade_dt:datetime) -> int:    # hxxjava add
        """ 
        get the index of a bar which the trade time belongs to.
        return:
            -1      : belongs to none
            0,1,... : bar's index 
        """

        a1 = np.array(sorted(self._datetime_index_map.keys()))
        a2 = a1 <= trade_dt

        return np.sum(a2 == True) - 1

当然别忘了在该文件的引用部分添加下面的语句

import numpy as np      # hxxjava add

2.2 绘图部件代码

from datetime import datetime
from typing import List, Tuple, Dict

from vnpy.trader.ui import create_qapp, QtCore, QtGui, QtWidgets
from pyqtgraph import ScatterPlotItem
import pyqtgraph as pg
import numpy as np
import talib
import copy

from vnpy.chart import ChartWidget, VolumeItem, CandleItem
from vnpy.chart.item import ChartItem
from vnpy.chart.manager import BarManager

from vnpy.trader.object import (
    BarData,
    OrderData,
    TradeData
)

from vnpy.trader.object import Direction, Exchange, Interval, Offset, Status, Product, OptionType, OrderType

class BarItem(CandleItem):
    """ 美国线 """
    BAR_WIDTH = 0.3

    def __init__(self, manager: BarManager):
        """"""
        super().__init__(manager)
        self.bar_pen: QtGui.QPen = pg.mkPen(color="w", width=2)
        self.bar_brush: QtGui.QBrush = pg.mkBrush(color="w")

    def _draw_bar_picture(self, ix: int, bar: BarData) -> QtGui.QPicture:
        """"""
        # Create objects
        candle_picture = QtGui.QPicture()
        painter = QtGui.QPainter(candle_picture)

        # Set painter color
        painter.setPen(self.bar_pen)
        painter.setBrush(self.bar_brush)

        open,high,low,close = bar.open_price,bar.high_price,bar.low_price,bar.close_price

        painter.drawLine(QtCore.QPointF(ix - self.BAR_WIDTH, open),QtCore.QPointF(ix, open))
        painter.drawLine(QtCore.QPointF(ix, high),QtCore.QPointF(ix, low))
        painter.drawLine(QtCore.QPointF(ix + self.BAR_WIDTH, close),QtCore.QPointF(ix, close))

        # Finish
        painter.end()
        return candle_picture

class LineItem(CandleItem):
    """"""

    def __init__(self, manager: BarManager):
        """"""
        super().__init__(manager)

        self.white_pen: QtGui.QPen = pg.mkPen(color=(255, 255, 255), width=1)

    def _draw_bar_picture(self, ix: int, bar: BarData) -> QtGui.QPicture:
        """"""
        last_bar = self._manager.get_bar(ix - 1)

        # Create objects
        picture = QtGui.QPicture()
        painter = QtGui.QPainter(picture)

        # Set painter color
        painter.setPen(self.white_pen)

        # Draw Line
        end_point = QtCore.QPointF(ix, bar.close_price)

        if last_bar:
            start_point = QtCore.QPointF(ix - 1, last_bar.close_price)
        else:
            start_point = end_point

        painter.drawLine(start_point, end_point)

        # Finish
        painter.end()
        return picture

    def get_info_text(self, ix: int) -> str:
        """"""
        text = ""
        bar = self._manager.get_bar(ix)
        if bar:
            text = f"Close:{bar.close_price}"
        return text

class SmaItem(CandleItem):
    """"""

    def __init__(self, manager: BarManager):
        """"""
        super().__init__(manager)

        self.line_pen: QtGui.QPen = pg.mkPen(color=(100, 100, 255), width=2)

        self.sma_window = 10
        self.sma_data: Dict[int, float] = {}

    def set_pen(self,pen:QtGui.QPen):
        """ 设置绘图的笔 """
        self.line_pen = pen

    def set_sma_window(self,sma_window:int):
        """ 设置Sma的窗口 """
        self.sma_window = sma_window

    def get_sma_value(self, ix: int) -> float:
        """"""
        if ix < 0:
            return 0

        # When initialize, calculate all rsi value
        if not self.sma_data:
            bars = self._manager.get_all_bars()
            close_data = [bar.close_price for bar in bars]
            sma_array = talib.SMA(np.array(close_data), self.sma_window)

            for n, value in enumerate(sma_array):
                self.sma_data[n] = value

        # Return if already calcualted
        if ix in self.sma_data:
            return self.sma_data[ix]

        # Else calculate new value
        close_data = []
        for n in range(ix - self.sma_window, ix + 1):
            bar = self._manager.get_bar(n)
            close_data.append(bar.close_price)

        sma_array = talib.SMA(np.array(close_data), self.sma_window)
        sma_value = sma_array[-1]
        self.sma_data[ix] = sma_value

        return sma_value

    def _draw_bar_picture(self, ix: int, bar: BarData) -> QtGui.QPicture:
        """"""
        sma_value = self.get_sma_value(ix)
        last_sma_value = self.get_sma_value(ix - 1)

        # Create objects
        picture = QtGui.QPicture()
        painter = QtGui.QPainter(picture)

        # Set painter color
        painter.setPen(self.line_pen)

        # Draw Line
        start_point = QtCore.QPointF(ix-1, last_sma_value)
        end_point = QtCore.QPointF(ix, sma_value)
        painter.drawLine(start_point, end_point)

        # Finish
        painter.end()
        return picture

    def get_info_text(self, ix: int) -> str:
        """"""
        if ix in self.sma_data:
            sma_value = self.sma_data[ix]
            text = f"SMA{self.sma_window} {sma_value:.1f}"
        else:
            text = "SMA{self.sma_window} -"

        return text

class RsiItem(ChartItem):
    """"""

    def __init__(self, manager: BarManager):
        """"""
        super().__init__(manager)

        self.white_pen: QtGui.QPen = pg.mkPen(color=(255, 255, 255), width=1)
        self.yellow_pen: QtGui.QPen = pg.mkPen(color=(255, 255, 0), width=2)

        self.rsi_window = 14
        self.rsi_data: Dict[int, float] = {}

    def get_rsi_value(self, ix: int) -> float:
        """"""
        if ix < 0:
            return 50

        # When initialize, calculate all rsi value
        if not self.rsi_data:
            bars = self._manager.get_all_bars()
            close_data = [bar.close_price for bar in bars]
            rsi_array = talib.RSI(np.array(close_data), self.rsi_window)

            for n, value in enumerate(rsi_array):
                self.rsi_data[n] = value

        # Return if already calcualted
        if ix in self.rsi_data:
            return self.rsi_data[ix]

        # Else calculate new value
        close_data = []
        for n in range(ix - self.rsi_window, ix + 1):
            bar = self._manager.get_bar(n)
            close_data.append(bar.close_price)

        rsi_array = talib.RSI(np.array(close_data), self.rsi_window)
        rsi_value = rsi_array[-1]
        self.rsi_data[ix] = rsi_value

        return rsi_value

    def _draw_bar_picture(self, ix: int, bar: BarData) -> QtGui.QPicture:
        """"""
        rsi_value = self.get_rsi_value(ix)
        last_rsi_value = self.get_rsi_value(ix - 1)

        # Create objects
        picture = QtGui.QPicture()
        painter = QtGui.QPainter(picture)

        # Draw RSI line
        painter.setPen(self.yellow_pen)

        if np.isnan(last_rsi_value) or np.isnan(rsi_value):
            # print(ix - 1, last_rsi_value,ix, rsi_value,)
            pass
        else:
            end_point = QtCore.QPointF(ix, rsi_value)
            start_point = QtCore.QPointF(ix - 1, last_rsi_value)
            painter.drawLine(start_point, end_point)

        # Draw oversold/overbought line
        painter.setPen(self.white_pen)

        painter.drawLine(
            QtCore.QPointF(ix, 70),
            QtCore.QPointF(ix - 1, 70),
        )

        painter.drawLine(
            QtCore.QPointF(ix, 30),
            QtCore.QPointF(ix - 1, 30),
        )

        # Finish
        painter.end()
        return picture

    def boundingRect(self) -> QtCore.QRectF:
        """"""
        # min_price, max_price = self._manager.get_price_range()
        rect = QtCore.QRectF(
            0,
            0,
            len(self._bar_picutures),
            100
        )
        return rect

    def get_y_range( self, min_ix: int = None, max_ix: int = None) -> Tuple[float, float]:
        """  """
        return 0, 100

    def get_info_text(self, ix: int) -> str:
        """"""
        if ix in self.rsi_data:
            rsi_value = self.rsi_data[ix]
            text = f"RSI {rsi_value:.1f}"
            # print(text)
        else:
            text = "RSI -"

        return text

def to_int(value: float) -> int:
    """"""
    return int(round(value, 0))

def adjust_range(in_range:Tuple[float, float])->Tuple[float, float]:
    """ 将y方向的显示范围扩大到1.1 """
    ret_range:Tuple[float, float]
    diff = abs(in_range[0] - in_range[1])
    ret_range = (in_range[0]-diff*0.05,in_range[1]+diff*0.05)
    return ret_range

class MacdItem(ChartItem):
    """"""
    _values_ranges: Dict[Tuple[int, int], Tuple[float, float]] = {}

    last_range:Tuple[int, int] = (-1,-1)    # 最新显示K线索引范围

    def __init__(self, manager: BarManager):
        """"""
        super().__init__(manager)

        self.white_pen: QtGui.QPen = pg.mkPen(color=(255, 255, 255), width=1)
        self.yellow_pen: QtGui.QPen = pg.mkPen(color=(255, 255, 0), width=1)
        self.red_pen: QtGui.QPen = pg.mkPen(color=(255, 0, 0), width=1)
        self.green_pen: QtGui.QPen = pg.mkPen(color=(0, 255, 0), width=1)

        self.short_window = 12
        self.long_window = 26
        self.M = 9

        self.macd_data: Dict[int, Tuple[float,float,float]] = {}

    def get_macd_value(self, ix: int) -> Tuple[float,float,float]:
        """"""
        if ix < 0:
            return (0.0,0.0,0.0)

        # When initialize, calculate all macd value
        if not self.macd_data:
            bars = self._manager.get_all_bars()
            close_data = [bar.close_price for bar in bars]

            diffs,deas,macds = talib.MACD(np.array(close_data), 
                                    fastperiod=self.short_window, 
                                    slowperiod=self.long_window, 
                                    signalperiod=self.M)

            for n in range(0,len(diffs)):
                self.macd_data[n] = (diffs[n],deas[n],macds[n])

        # Return if already calcualted
        if ix in self.macd_data:
            return self.macd_data[ix]

        # Else calculate new value
        close_data = []
        for n in range(ix-self.long_window-self.M+1, ix + 1):
            bar = self._manager.get_bar(n)
            close_data.append(bar.close_price)

        diffs,deas,macds = talib.MACD(np.array(close_data), 
                                            fastperiod=self.short_window, 
                                            slowperiod=self.long_window, 
                                            signalperiod=self.M) 
        diff,dea,macd = diffs[-1],deas[-1],macds[-1]
        self.macd_data[ix] = (diff,dea,macd)

        return (diff,dea,macd)

    def _draw_bar_picture(self, ix: int, bar: BarData) -> QtGui.QPicture:
        """"""
        macd_value = self.get_macd_value(ix)
        last_macd_value = self.get_macd_value(ix - 1)

        # # Create objects
        picture = QtGui.QPicture()
        painter = QtGui.QPainter(picture)

        # # Draw macd lines
        if np.isnan(macd_value[0]) or np.isnan(last_macd_value[0]):
            # print("略过macd lines0")
            pass
        else:
            end_point0 = QtCore.QPointF(ix, macd_value[0])
            start_point0 = QtCore.QPointF(ix - 1, last_macd_value[0])
            painter.setPen(self.white_pen)
            painter.drawLine(start_point0, end_point0)

        if np.isnan(macd_value[1]) or np.isnan(last_macd_value[1]):
            # print("略过macd lines1")
            pass
        else:
            end_point1 = QtCore.QPointF(ix, macd_value[1])
            start_point1 = QtCore.QPointF(ix - 1, last_macd_value[1])
            painter.setPen(self.yellow_pen)
            painter.drawLine(start_point1, end_point1)

        if not np.isnan(macd_value[2]):
            if (macd_value[2]>0):
                painter.setPen(self.red_pen)
                painter.setBrush(pg.mkBrush(255,0,0))
            else:
                painter.setPen(self.green_pen)
                painter.setBrush(pg.mkBrush(0,255,0))
            painter.drawRect(QtCore.QRectF(ix-0.3,0,0.6,macd_value[2]))
        else:
            # print("略过macd lines2")
            pass

        painter.end()
        return picture

    def boundingRect(self) -> QtCore.QRectF:
        """"""
        min_y, max_y = self.get_y_range()
        rect = QtCore.QRectF(
            0,
            min_y,
            len(self._bar_picutures),
            max_y
        )
        return rect

    def get_y_range(self, min_ix: int = None, max_ix: int = None) -> Tuple[float, float]:
        #   获得3个指标在y轴方向的范围   
        #   hxxjava 修改,2020-6-29
        #   当显示范围改变时,min_ix,max_ix的值不为None,当显示范围不变时,min_ix,max_ix的值不为None,

        offset = max(self.short_window,self.long_window) + self.M - 1

        if not self.macd_data or len(self.macd_data) < offset:
            # print(f'(min_ix,max_ix){(min_ix,max_ix)} offset={offset},len(self.macd_data)={len(self.macd_data)}')
            # hxxjava 修改,2021-5-8,因为升级vnpy,其依赖的pyqtgraph版本也升级了,原来为return 0,1
            return -100, 100

        # print("len of range dict:",len(self._values_ranges),",macd_data:",len(self.macd_data),(min_ix,max_ix))

        if min_ix != None:          # 调整最小K线索引
            min_ix = max(min_ix,offset)

        if max_ix != None:          # 调整最大K线索引
            max_ix = min(max_ix, len(self.macd_data)-1)

        last_range = (min_ix,max_ix)    # 请求的最新范围   

        if last_range == (None,None):   # 当显示范围不变时
            if self.last_range in self._values_ranges:  
                # 如果y方向范围已经保存
                # 读取y方向范围
                result = self._values_ranges[self.last_range]
                # print("1:",self.last_range,result)
                return adjust_range(result)
            else:
                # 如果y方向范围没有保存
                # 从macd_data重新计算y方向范围
                min_ix,max_ix = 0,len(self.macd_data)-1

                macd_list = list(self.macd_data.values())[min_ix:max_ix + 1]
                ndarray = np.array(macd_list)           
                max_price = np.nanmax(ndarray)
                min_price = np.nanmin(ndarray)

                # 保存y方向范围,同时返回结果
                result = (min_price, max_price)
                self.last_range = (min_ix,max_ix)
                self._values_ranges[self.last_range] = result
                # print("2:",self.last_range,result)
                return adjust_range(result)

        """ 以下为显示范围变化时 """

        if last_range in self._values_ranges:
            # 该范围已经保存过y方向范围
            # 取得y方向范围,返回结果
            result = self._values_ranges[last_range]
            # print("3:",last_range,result)
            return adjust_range(result)

        # 该范围没有保存过y方向范围
        # 从macd_data重新计算y方向范围
        macd_list = list(self.macd_data.values())[min_ix:max_ix + 1]
        ndarray = np.array(macd_list) 
        max_price = np.nanmax(ndarray)
        min_price = np.nanmin(ndarray)

        # 取得y方向范围,返回结果
        result = (min_price, max_price)

        self.last_range = last_range
        self._values_ranges[self.last_range] = result
        # print("4:",self.last_range,result)

        return adjust_range(result)


    def get_info_text(self, ix: int) -> str:
        """ """
        barscount = len(self._manager._bars) # hxxjava debug
        if ix in self.macd_data:
            diff,dea,macd = self.macd_data[ix]
            words = [
                f"diff {diff:.3f}"," ",
                f"dea {dea:.3f}"," ",
                f"macd {macd:.3f}",
                f"barscount={ix,barscount}"
                ]
            text = "\n".join(words)
        else:
            text = "diff - \ndea - \nmacd -"

        return text

def tip_func(x,y,data):
    """ """
    return f"{data}"

class BaseScatter(pg.ScatterPlotItem):
    """  """
    def __init__(self, plot:pg.PlotItem,manager:BarManager,*args, **kargs):
        """  """
        super().__init__(args=args,kargs=kargs)
        self.plot = plot
        self.manager = manager
        self.plot.addItem(self)

        self.opts['hoverable'] = True


    def hoverEvent(self, ev):
        """  """
        if self.opts['hoverable']:
            old = self.data['hovered']

            if ev.exit:
                new = np.zeros_like(self.data['hovered'])
            else:
                new = self._maskAt(ev.pos())

            if self._hasHoverStyle():
                self.data['sourceRect'][old ^ new] = 0
                self.data['hovered'] = new
                self.updateSpots()

            points = self.points()[new][::-1]

            # Show information about hovered points in a tool tip
            vb = self.getViewBox()
            if vb is not None and self.opts['tip'] is not None:
                cutoff = 10
                # tip = [self.opts['tip'](x=pt.pos().x(), y=pt.pos().y(), data=pt.data())
                tip = [tip_func(x=pt.pos().x(), y=pt.pos().y(), data=pt.data()) for pt in points[:cutoff]]
                if len(points) > cutoff:
                    tip.append('({} others...)'.format(len(points) - cutoff))
                vb.setToolTip('\n\n'.join(tip))

            self.sigHovered.emit(self, points, ev)

class TradeItem(BaseScatter):
    """ 成交单绘图部件 """

    TRADE_COLOR_MAP = {
        (Direction.LONG,Offset.OPEN):'red',
        (Direction.LONG,Offset.CLOSE):'magenta',
        (Direction.LONG,Offset.CLOSETODAY):'magenta',
        (Direction.LONG,Offset.CLOSEYESTERDAY):'magenta',
        (Direction.SHORT,Offset.OPEN):'green',
        (Direction.SHORT,Offset.CLOSE):'yellow',
        (Direction.SHORT,Offset.CLOSETODAY):'yellow',
        (Direction.SHORT,Offset.CLOSEYESTERDAY):'yellow',
    }

    TRADE_COMMAND_MAP = {
        (Direction.LONG,Offset.OPEN):'买开',
        (Direction.LONG,Offset.CLOSE):'买平',
        (Direction.LONG,Offset.CLOSETODAY):'买平今',
        (Direction.LONG,Offset.CLOSEYESTERDAY):'买平昨',
        (Direction.SHORT,Offset.OPEN):'卖开',
        (Direction.SHORT,Offset.CLOSE):'卖平',
        (Direction.SHORT,Offset.CLOSETODAY):'卖平今',
        (Direction.SHORT,Offset.CLOSEYESTERDAY):'卖平昨',
    }

    def __init__(self, plot:pg.PlotItem,manager:BarManager):
        """ """
        super().__init__(plot=plot,manager=manager,size=15, pxMode=True,pen=pg.mkPen(None), brush=pg.mkBrush(255, 255, 255, 120))
        self.trades : List = []

    def _to_scatter_data(self,trade:TradeData):
        """ """
        idx = self.manager.get_bar_idx(trade.datetime)
        if idx == -1:
            return {}

        bar:BarData = self.manager.get_bar(idx)

        color = self.TRADE_COLOR_MAP[(trade.direction,trade.offset)]

        size = 10
        LL,HH = self.manager.get_price_range()
        y_adjustment = (HH-LL) * 0.01

        if trade.direction == Direction.LONG:
            symbol = 't1'
            y = bar.low_price - y_adjustment 

        else:
            symbol = 't'
            y = bar.high_price + y_adjustment

        # pen = pg.mkPen(QtGui.QColor(color))
        # brush = pg.mkBrush(QtGui.QColor(color))

        scatter_data = {
            "pos": (idx, y),
            "size": size,
            "pen": color,
            "brush": color,
            "symbol": symbol,
            "data": "成交单:{},单号:{},指令:{},价格:{},手数:{},时间:{}".format(
                trade.vt_symbol,
                trade.vt_tradeid,
                self.TRADE_COMMAND_MAP[(trade.direction,trade.offset)],
                trade.price,trade.volume,
                trade.datetime.strftime('%Y-%m-%d %H:%M:%S')
            )
        }

        return scatter_data

    def add_trades(self, trades: List[TradeData]):
        """"""
        # 将trade转换为scatter数据
        # self.updated = False

        self.trades.extend(trades)

        spots = []
        for trade in self.trades:
            scatter = self._to_scatter_data(trade)
            if not scatter:
                continue
            spots.append(scatter)

        # self.clear()
        # self.plot.removeItem(self)
        self.setData(spots,hoverable=True)

    def add_trade(self,trade:TradeData):
        """ """
        self.trades.append(trade)

        spots = []
        for trade in self.trades:
            scatter = self._to_scatter_data(trade)
            if not scatter:
                continue
            spots.append(scatter)

        # self.clear()
        # self.plot.removeItem(self)
        self.setData(spots,hoverable=True)

class OrderItem(BaseScatter):
    """ 成交单绘图部件 """

    ORDER_COLOR_MAP = {
        (Direction.LONG,Offset.OPEN):'red',
        (Direction.LONG,Offset.CLOSE):'magenta',
        (Direction.LONG,Offset.CLOSETODAY):'magenta',
        (Direction.LONG,Offset.CLOSEYESTERDAY):'magenta',
        (Direction.SHORT,Offset.OPEN):'green',
        (Direction.SHORT,Offset.CLOSE):'yellow',
        (Direction.SHORT,Offset.CLOSETODAY):'yellow',
        (Direction.SHORT,Offset.CLOSEYESTERDAY):'yellow',
    }

    ORDER_COMMAND_MAP = {
        (Direction.LONG,Offset.OPEN):'买开',
        (Direction.LONG,Offset.CLOSE):'买平',
        (Direction.LONG,Offset.CLOSETODAY):'买平今',
        (Direction.LONG,Offset.CLOSEYESTERDAY):'买平昨',
        (Direction.SHORT,Offset.OPEN):'卖开',
        (Direction.SHORT,Offset.CLOSE):'卖平',
        (Direction.SHORT,Offset.CLOSETODAY):'卖平今',
        (Direction.SHORT,Offset.CLOSEYESTERDAY):'卖平昨',
    }

    def __init__(self, plot:pg.PlotItem,manager:BarManager):
        """ """
        super().__init__(plot=plot,manager=manager,size=15, pxMode=True,pen=pg.mkPen(None), brush=pg.mkBrush(255, 255, 255, 120))

        self.orders : List[OrderData] = []

    def _to_scatter_data(self,order:OrderData):
        """ """
        if not order.datetime:
            return {}
        idx = self.manager.get_bar_idx(order.datetime)
        if idx == -1:
            return {}

        bar:BarData = self.manager.get_bar(idx)

        color = self.ORDER_COLOR_MAP[(order.direction,order.offset)]

        size = 10
        LL,HH = self.manager.get_price_range()
        y_adjustment = (HH-LL) * 0.02

        if order.direction == Direction.LONG:
            symbol = 'o'
            y = bar.low_price - y_adjustment 

        else:
            symbol = 'o'
            y = bar.high_price + y_adjustment

        # pen = pg.mkPen(QtGui.QColor(color))
        # brush = pg.mkBrush(QtGui.QColor(color))

        scatter_data = {
            "pos": (idx, y),
            "size": size,
            "pen": color,
            "brush": color,
            "symbol": symbol,
            "data": "委托单:{},单号:{},指令:{},价格:{},手数:{},时间:{}".format(
                order.vt_symbol,
                order.vt_orderid,
                self.ORDER_COMMAND_MAP[(order.direction,order.offset)],
                order.price,order.volume,
                order.datetime.strftime('%Y-%m-%d %H:%M:%S')
            )
        }

        return scatter_data


    def add_orders(self, orders: List[OrderData]):
        """"""
        # 将trade转换为scatter数据
        # self.updated = False
        filter_orders = [order for order in orders if order.datetime is not None and order.traded > 0]
        if not filter_orders:
            return 
        self.orders.extend(filter_orders)

        spots = []
        for order in self.orders:
            scatter = self._to_scatter_data(order)
            if not scatter:
                continue
            spots.append(scatter)

        print(f"spots={spots}")

        # self.clear()
        # self.plot.removeItem(self)
        self.setData(spots,hoverable=True)

    def add_order(self,order:OrderData):
        """ """
        if order.datetime is None or order.traded == 0:
            return

        self.orders.append(order)

        spots = []
        for order in self.orders:
            scatter = self._to_scatter_data(order)
            if not scatter:
                continue
            spots.append(scatter)

        print(f"spots={spots}")

        # self.clear()
        # self.plot.removeItem(self)
        self.setData(spots,hoverable=True)

3. OrderItem和TradeItem使用注意事项

创建OrderItem和TradeItem时,必须传递主图或者附图的plot和bar管理器BarManager,示例代码如下:

        candle_plot = self.chart.get_plot('candle')
        manager = self.chart._manager

        self.trade_item:TradeItem = TradeItem(plot=candle_plot,manager=manager)

4. 移动到成交单图标上会有提示

当十字光标移动到成交单图标时,如果当根K线上发生过多次成交,你可能只看见一个图标,但其实是有多个图标被绘制的,这反应在图中的光标提示中,如图所示:

description

5. 再增加一个美国线指标BarItem

看效果图:

description

1. 错误现象的发现

1.1 假定您是这么做的:

  • 您在交易日的盘前某个时间(如20:50)启动VNStation,
  • 您备使用CTA策略模块运行你CTA策略;
  • 用鼠标点击您创建好的策略实例界面的“初始化”按钮;
  • 按照通常的做法,您的策略一般会加载一定天数的历史1分钟K线数据;
  • 过短暂的等待,策略的初始化完成了,标志是"启动"按钮由无效变成有效;
  • 用鼠标点击策略实例界面的“启动”按钮,OK,策略 启动完成了!

1.2 错误也发生了

尽管此时还是没有开盘,甚至还没有开始集合竞价,可是您的策略已经从on_tick()接口被推送了一个tick,而且该tick的时间不是当天下午的收盘,也不是您订阅该合约的时间 ! 我把这个tick打印出来了,请看:

TickData(
    gateway_name='CTP', 
    symbol='TA205', 
    exchange=<Exchange.CZCE: 'CZCE'>, 
    datetime=datetime.datetime(2022, 4, 12, 20, 1, 18, 500000, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>), 
    name='精对苯二甲酸205', 
    volume=0, 
    turnover=0.0, 
    open_interest=441112.0, 
    last_price=6128.0, 
    last_volume=0, 
    limit_up=6374.0, 
    limit_down=5538.0, 
    open_price=0, 
    high_price=0, 
    low_price=0, 
    pre_close=6128.0, 
    bid_price_1=0, 
    bid_price_2=0, 
    bid_price_3=0, 
    bid_price_4=0, 
    bid_price_5=0, 
    ask_price_1=0, 
    ask_price_2=0, 
    ask_price_3=0, 
    ask_price_4=0, 
    ask_price_5=0, 
    bid_volume_1=0, 
    bid_volume_2=0, 
    bid_volume_3=0, 
    bid_volume_4=0, 
    bid_volume_5=0, 
    ask_volume_1=0, 
    ask_volume_2=0, 
    ask_volume_3=0, 
    ask_volume_4=0, 
    ask_volume_5=0, 
    localtime=None
)

请注意,该tick的时间是:2022-4-12 20:1:18.500000 !!!

很快开始集合竞价,在20:59的时候,策略可能又会收到一个包含开盘价的tick。
1分钟后是21:00,正式进入连续竞价阶段,策略又会收到等多的tick。

为了后面叙述的方便,我们把20:50时收到tick叫tick1,20:59时收到tick叫tick2。

假如CTA策略使用了30分钟K线,那么随着集合竞价结束,在21:00的时候,策略中的BarGeneraor对象,就会为您生成两个莫名其妙的30分钟K线:

  • 20:59时,生成第一个30分钟K线,它只包含一个tick1,
  • 21:00时,生成第二个30分钟K线,它只包含一个tick2,

启动策略不到10分钟时间,就已经虚多了2个30分钟K线。

这是错误的!!!

1.3 这不是偶然现象,它是一定会发生的!

发生这种tick时间戳错误的时机:

  1. vnpy系统启动CTP网关,首次连接行情服务器接口CtpMdApi时
  2. 客户端已经接行过情服务器接口CtpMdApi,客户端因为网络问题断开了连接,再次自动或手动连接情服务器接口CtpMdApi时
  3. 客户端已经接行过情服务器接口CtpMdApi,交易所的行情服务器关闭或者重启,再次自动或手动连接情服务器接口CtpMdApi时

这种tick时间戳错误的表现:

  1. tick时间戳在有效交易时间段外,如可能是23:00、07:28或者07:45。
  2. tick时间戳在有效交易时间段内,如可能是9:08:03。这种情况可能是因为盘中客户端网络原因,再次连接情服务器接口CtpMdApi时造成的。这种情况最麻烦!因为你无法通过时间戳的特征去辨别出其是否为无效的tick,但是该tick有可能已经在断开之前已经收到过了,只是重新连接之后被冠以重连CtpMdApi接口之时的时间戳再次发生给客户端,这会导致我们合成出的1分钟K线的错误。如果1分钟K线无法保证正确,由1分钟K线合成的n分钟、n小时、日K线也是无法保证的。

2. CtaEngine对策略的初始化过程

CTA策略的初始化是由CtaEngine驱动的,其执行逻辑在vnpy_ctastrategy\engine的CtaEngine._init_strategy()中:

    def _init_strategy(self, strategy_name: str):
        """
        Init strategies in queue.
        """
        strategy = self.strategies[strategy_name]

        if strategy.inited:
            self.write_log(f"{strategy_name}已经完成初始化,禁止重复操作")
            return

        self.write_log(f"{strategy_name}开始执行初始化")

        # Call on_init function of strategy
        self.call_strategy_func(strategy, strategy.on_init)

        # Restore strategy data(variables)
        data = self.strategy_data.get(strategy_name, None)
        if data:
            for name in strategy.variables:
                value = data.get(name, None)
                if value is not None:
                    setattr(strategy, name, value)

        # Subscribe market data
        contract = self.main_engine.get_contract(strategy.vt_symbol)
        if contract:
            req = SubscribeRequest(
                symbol=contract.symbol, exchange=contract.exchange)
            self.main_engine.subscribe(req, contract.gateway_name)
        else:
            self.write_log(f"行情订阅失败,找不到合约{strategy.vt_symbol}", strategy)

        # Put event to update init completed status.
        strategy.inited = True
        self.put_strategy_event(strategy)
        self.write_log(f"{strategy_name}初始化完成")

_init_strategy()执行过程是先为策略加载历史数据,再订阅策略交易合约的行情。

3. 怎么解决这些问题?

要想解决问题,就必须问题的根源在哪里?

3.1 多出来的第一个30分钟K线的原因

因为订阅合约行情执行的是CtpMdApi的subscribe():

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

self.subscribeMarketData(req.symbol)只要是首次订阅,接口都会立即从OnRtnDepthMarketData推送1个该合约最新的深度行情通知,而时间是:

    ///最后修改时间
    TThostFtdcTimeType   UpdateTime;
    ///最后修改毫秒
    TThostFtdcMillisecType   UpdateMillisec;

这里的最后修改时间和最后修改毫秒本应该是该合约最后交易的时间,也可能是交易所行情服务器中CTP行情接口重新打开的时间!这就是为什么我们开动tick1的时间是2022-4-12 20:1:18.500000的原因。

3.2 多出来的第二个30分钟K线的原因

TA205.CZCE在每个交易日的集合竞价时段的第4分钟会产生一个集合竞价tick。
你可能会说这个没有毛病,从20:30~21:00,确实是可以生成一个30分钟K线,为什么它不可以只包含一个tick呢?
这么说也过得去,可是问题是咱们在加载其他历史数据的时候,无论我们使用米筐、tushare或者什么其他第三方历史数据时,加载的1分钟K线,从来都没有这样的数据。
或者我们把策略产生的30分钟K线与通达信、大智慧或者文华6等软件生成的30分钟K线比较一下,它们都是没有出现这第二个30分钟K线情况的。从这种种也可以看出来这个tick的处理是不对的,tick2必须归入到21:00~21:30。

3.3 一副K线图表截图

description

从前面所说,无论你使用什么样的BarGenerator来生成K线,离开了合约交易时间段,仅仅用时间特征去合成K线,那么一定会在连接实际行情接口的时候出现上述错误!

如果说vnpy自带的BarGenerator在合成由第三方提供的历史K线还撮合着能够用的话,那也是因为所有的无效数据是由第三方为您过滤和清新掉了,掩盖了BarGenerator的问题而已!!!

3.4 策略在收到tick推送的时候,必须先进行有效性过滤

  • tick时间戳必须在有效时段内,否则做丢弃处理
  • tick时间戳在集合竞价时段内,将它视作其后的第一个连续交易时段的开始时间
  • 对tick时间戳在交易日中间的休市时段内的,将它视作其之前一个连续交易时段的结束时间
  • 对tick时间戳在交易日中间的休市时段内的,将它视作其之前一个连续交易时段的结束时间
    .

1. 深度行情接口OnRtnDepthMarketData的参数是这样的

struct CThostFtdcDepthMarketDataField
{
    ///交易日
    TThostFtdcDateType   TradingDay;
    ///合约代码
    TThostFtdcInstrumentIDType  InstrumentID;
    ///交易所代码
    TThostFtdcExchangeIDType ExchangeID;
    ///合约在交易所的代码
    TThostFtdcExchangeInstIDType    ExchangeInstID;
    ///最新价
    TThostFtdcPriceType  LastPrice;
    ///上次结算价
    TThostFtdcPriceType  PreSettlementPrice;
    ///昨收盘
    TThostFtdcPriceType  PreClosePrice;
    ///昨持仓量
    TThostFtdcLargeVolumeType   PreOpenInterest;
    ///今开盘
    TThostFtdcPriceType  OpenPrice;
    ///最高价
    TThostFtdcPriceType  HighestPrice;
    ///最低价
    TThostFtdcPriceType  LowestPrice;
    ///数量
    TThostFtdcVolumeType Volume;
    ///成交金额
    TThostFtdcMoneyType  Turnover;
    ///持仓量
    TThostFtdcLargeVolumeType   OpenInterest;
    ///今收盘
    TThostFtdcPriceType  ClosePrice;
    ///本次结算价
    TThostFtdcPriceType  SettlementPrice;
    ///涨停板价
    TThostFtdcPriceType  UpperLimitPrice;
    ///跌停板价
    TThostFtdcPriceType  LowerLimitPrice;
    ///昨虚实度
    TThostFtdcRatioType  PreDelta;
    ///今虚实度
    TThostFtdcRatioType  CurrDelta;
    ///最后修改时间
    TThostFtdcTimeType   UpdateTime;
    ///最后修改毫秒
    TThostFtdcMillisecType   UpdateMillisec;
    ///申买价一
    TThostFtdcPriceType  BidPrice1;
    ///申买量一
    TThostFtdcVolumeType BidVolume1;
    ///申卖价一
    TThostFtdcPriceType  AskPrice1;
    ///申卖量一
    TThostFtdcVolumeType AskVolume1;
    ///申买价二
    TThostFtdcPriceType  BidPrice2;
    ///申买量二
    TThostFtdcVolumeType BidVolume2;
    ///申卖价二
    TThostFtdcPriceType  AskPrice2;
    ///申卖量二
    TThostFtdcVolumeType AskVolume2;
    ///申买价三
    TThostFtdcPriceType  BidPrice3;
    ///申买量三
    TThostFtdcVolumeType BidVolume3;
    ///申卖价三
    TThostFtdcPriceType  AskPrice3;
    ///申卖量三
    TThostFtdcVolumeType AskVolume3;
    ///申买价四
    TThostFtdcPriceType  BidPrice4;
    ///申买量四
    TThostFtdcVolumeType BidVolume4;
    ///申卖价四
    TThostFtdcPriceType  AskPrice4;
    ///申卖量四
    TThostFtdcVolumeType AskVolume4;
    ///申买价五
    TThostFtdcPriceType  BidPrice5;
    ///申买量五
    TThostFtdcVolumeType BidVolume5;
    ///申卖价五
    TThostFtdcPriceType  AskPrice5;
    ///申卖量五
    TThostFtdcVolumeType AskVolume5;
    ///当日均价
    TThostFtdcPriceType  AveragePrice;
    ///业务日期
    TThostFtdcDateType   ActionDay;
};

其中 UpdateMillisec 为最后修改毫秒,int型

2. CtpMdApi中该接口函数中对日期处理错误、对毫秒处理不合适

错误和不合适之处已经改正,见注释:

    def onRtnDepthMarketData(self, data: dict) -> None:
        """行情数据推送"""
        # 过滤没有时间戳的异常行情数据
        if not data["UpdateTime"]:
            return

        # 过滤还没有收到合约数据前的行情推送
        symbol: str = data["InstrumentID"]
        contract: ContractData = symbol_contract_map.get(symbol, None)
        if not contract:
            return

        # 对大商所的交易日字段取本地日期
        if not data["ActionDay"] or contract.exchange == Exchange.DCE:
            # 这里废了那么大的劲,却使用了一个更新滞后的变量,属实不应该
            # self.current_date是由定时器几秒更新一次,
            # 对于一些跨夜品种,会导致几秒钟的tick的日期错误
            # date_str: str = self.current_date   
            date_str: str = datetime.now().strftime("%Y%m%d")    # hxxjava change
        else:
            date_str: str = data["ActionDay"]

        # 这里不好,为什么要故意降低接口的时间精度,放着毫秒不要而费劲地变化为0.1秒精度?
        # timestamp: str = f"{date_str} {data['UpdateTime']}.{int(data['UpdateMillisec']/100)}"        
        timestamp: str = f"{date_str} {data['UpdateTime']}." + str(data['UpdateMillisec']*1000).zfill(6) # hxxjava edit
        dt: datetime = datetime.strptime(timestamp, "%Y%m%d %H:%M:%S.%f")
        dt: datetime = CHINA_TZ.localize(dt)

        tick: TickData = TickData(
            symbol=symbol,
            exchange=contract.exchange,
            datetime=dt,
            name=contract.name,
            volume=data["Volume"],
            turnover=data["Turnover"],
            open_interest=data["OpenInterest"],
            last_price=adjust_price(data["LastPrice"]),
            limit_up=data["UpperLimitPrice"],
            limit_down=data["LowerLimitPrice"],
            open_price=adjust_price(data["OpenPrice"]),
            high_price=adjust_price(data["HighestPrice"]),
            low_price=adjust_price(data["LowestPrice"]),
            pre_close=adjust_price(data["PreClosePrice"]),
            bid_price_1=adjust_price(data["BidPrice1"]),
            ask_price_1=adjust_price(data["AskPrice1"]),
            bid_volume_1=data["BidVolume1"],
            ask_volume_1=data["AskVolume1"],
            gateway_name=self.gateway_name
        )

        if data["BidVolume2"] or data["AskVolume2"]:
            tick.bid_price_2 = adjust_price(data["BidPrice2"])
            tick.bid_price_3 = adjust_price(data["BidPrice3"])
            tick.bid_price_4 = adjust_price(data["BidPrice4"])
            tick.bid_price_5 = adjust_price(data["BidPrice5"])

            tick.ask_price_2 = adjust_price(data["AskPrice2"])
            tick.ask_price_3 = adjust_price(data["AskPrice3"])
            tick.ask_price_4 = adjust_price(data["AskPrice4"])
            tick.ask_price_5 = adjust_price(data["AskPrice5"])

            tick.bid_volume_2 = data["BidVolume2"]
            tick.bid_volume_3 = data["BidVolume3"]
            tick.bid_volume_4 = data["BidVolume4"]
            tick.bid_volume_5 = data["BidVolume5"]

            tick.ask_volume_2 = data["AskVolume2"]
            tick.ask_volume_3 = data["AskVolume3"]
            tick.ask_volume_4 = data["AskVolume4"]
            tick.ask_volume_5 = data["AskVolume5"]

        self.gateway.on_tick(tick)

先厘清大思路,后面逐步完成。

1.搞量化交易,一个好用的K线生成器是最基本的要求!

vnpy系统自带了一个BarGenerator,它可以帮助我们生成1分钟,n分钟,n小时,日周期的K线,也叫bar。可是除了1分钟比较完美之外,有很多问题。它在读取历史数据、回测的时候多K线的处理和实盘却有不一样的效果。具体的问题我已经在解决vnpy 2.9.0版本的BarGenerator产生30分钟Bar的错误!这个帖子中做过尝试,但也不是很成功。因为系统的BarGenerator靠时间窗口与1分钟bar的时间分钟关系来决定是否该新建和结束一个bar,这个有问题。于是我改用对1分钟bar进行计数来决定是否该新建和结束一个bar,这也是有不可靠的问题,遇到行情比较清淡的时候,可能有的分钟就没有1分钟bar产生,这是完全有可能的!
K线几乎是绝大部分交易策略分析的基础,除非你从事的是极高频交易,否则你就得用它。可是如果你连生成一个稳健可靠的K线都不能够保证,那么运行在K线基础上的指标及由此产生的交易信号就无从谈起,K线错了,它们就是错误的,以此为依据做出点交易指令有可能是南辕北辙,所以必须解决它!

2.日内对齐等交易时长K线需要什么参数

2.1 日内对齐等交易时长K线是最常用的

K线不是交易所发布的,它有很多种产生机制。其对齐方式、表现形式多种多样。关于K线的分类本人在以往的帖子中做出过比较详细的说明,有兴趣的读者可以去我以往的帖子中查看,这里就不再赘述。
市面上的绝大部分软件如通达信、大智慧、文华财经等软件,除非用户特别设定,他们最常提供给用户的K线多是日内对齐等交易时长K线。常用是一定是有道理的,因为它们已经为广大用户和投资者所接受。

2.2 日内对齐等交易时长K线需要什么参数

1)什么是日内对齐等交易时长K线?
它具有这些特点:以每日开盘为起点,每根K线都包含相同交易时间的数据,跳过中间的休市时间,直至当前交易日的收盘,收盘不足n分钟也就是K线。实盘中,每日的第一个n分钟K线含集合竞价的那个tick数据。
2)为什么这种K线能够被普遍接受?
为它尽可能地保证一个交易日内的所有K线所表达的意义内容上是一致的,它们包含相等的交易时长。这非常重要,因为你把一个5分钟时长的K线与一个30分钟时长的K线放在一起谈论是没有意义的。但是如果为了保证K线在交易时长上的一致性,让n分钟K线跨日的话也是不太合理,因为这跨日,跨周末时间太长,这中间会发生什么意外事情,可能会产生出非常巨大的幅度大K线,掩盖了隔日跳空的行情变化,这对解读行情是不利的。当然n日的K线日跨日的,但是它是n个交易日的K线融合而成的,不过其融合的每个日K线也是对齐各自的日开盘的。
另外日内对齐等交易时长K线还有一个好处,那就是你以任何之前的时间为起点,在读取历史数据重新生成该日的n分钟K线的时候,得到的改日的K线是一致的。举个例子,如果我们的CTA策略在init()中常常是这么一句:

self.load_bar(20)  # 先加载20日历史1分钟数据

这么简单的一句,包含着很多你意识不到的变化——你今天运行策略和明天运行你的策略,其中的历史数据的范围发生了变化,也就是说加载数据的起点变了。如果我们合成的K线的对齐方式不采用日内对齐的话,而采用对齐加载时间起点的话,你今天、明天加载出来之前的某日的K线就可能完全是不同的。而采用日内对齐等交易时长的K线则不存在这个问题。

3)需要知道合约的交易时间段
既然要对齐每日开盘,还有跳过各个休市时间,还要知道收市时间,那么我们就知道生成这种K线必须知道其所表达合约或对象的交易时间段,交易时间段中包含了这些信息,不知道这些信息,BarGenerator就不知道如何生成这种bar。这是必须的!

3. 如何获取合约的交易时间段

3.1 vnpy系统和CTP接口找不到交易时间段信息

目前vnpy系统中的是没有合约的交易时间段的。到哪里获取合约的交易时间段的呢?
1) 它与合约相关,应该到保存合约的数据类ContractData中去找,没有找到。
2) 是否可以提供接口,从交易所获得,这个也是比较基础的数据。于是到CTP接口中(我使用的是CTP接口,您也许不一样) ,在最新版本的CTP接口文档中也没有找到任何与交易时间段相关的信息,绝望!

3.2 有两种方法可以得到交易时间段信息

  1. 米筐数据接口中有,其中有个得到市场中所有合约的函数all_instruments(),它的返回值中就包含交易时间段信息trading_hours,还有另外一些如get_trading_hours()也可以直接获得这些交易时间段信息,好!
  2. 实在没有的话,咱们手工创建一个文件,按照一定格式,把我们需要创建K线的合约准备好交易时间段信息,这也是可行的。

3.3 直接从米筐数据接口获取交易时间段信息的问题

  1. 你必须购买过米筐数,否则无从获得
  2. 直接使用从米筐数据接口获取交易时间段信息,会有效率问题。本人试过,运行get_trading_hours()常会用卡顿,其目前是在65000多个合约中为你寻找一个合约的交易时间段信息,在K线合成这种使用非常频繁的地方,效率是必须的!况且米筐对每个用户的流量也是有限制的,如果超过了也是会限流的!
  3. 对于有些米筐没有的品种难道我们就不能使用K线了吗?

解决方法:

  1. 基于以上这些原因,采用解耦具体数据提供商的方法会更好!把这些信息保存到一个文件或者数据库中,只要您能够办法获得这些信息,按照规定的格式存储,哪怕是手工输入也是可以的。
  2. 新的K线生成器只需要对规定格式的交易时间段信息进行处理,按照一定的规则就可以进行K线生成了!

3.4 从米筐获取交易时间段信息

3.4.1 扩展DataFeed

打开vnpy.trader.datafeed.py文件为Datafeed的基类BaseDatafeed扩展下面的接口

class BaseDatafeed(ABC):
    """
    Abstract datafeed class for connecting to different datafeed.
    """

    def init(self) -> bool:
        """
        Initialize datafeed service connection.
        """
        pass

    def update_all_trading_hours(self) -> bool:     # hxxjava add 
        """ 更新所有合约的交易时间段到trading_hours.json文件中 """
        pass

    def load_all_trading_hours(self) -> dict:       # hxxjava add 
        """ 从trading_hours.json文件中读取所有合约的交易时间段 """
        pass

    def query_bar_history(self, req: HistoryRequest) -> Optional[List[BarData]]:
        """
        Query history bar data.
        """
        pass

    def query_tick_history(self, req: HistoryRequest) -> Optional[List[TickData]]:
        """
        Query history tick data.
        """
        pass

其中的trading_hours.json文件我会在后面的文章中做详细的介绍。有了它我们才能展开其他的设计。

3.4.2 扩展RqdataDataFeed

在vnpy_rqdata\rqdata_datafeed.py中增加下面的代码

  • 引用部分增加:
from datetime import timedelta,date # hxxjava add
  • 在class RqdataDatafeed(BaseDatafeed)中增加下面的代码 :

    def update_all_trading_hours(self) -> bool:     # hxxjava add 
        """ 更新所有合约的交易时间段到trading_hours.json文件中 """

        if not self.inited:
            self.init()

        if not self.inited:
            return False

        ths_dict = load_json(self.trading_hours_file)

        # instruments = all_instruments(type=['Future','Stock','Index','Spot'])

        trade_hours = {}

        for stype in ['Future','Stock','Index','Fund','Spot']:
            instruments = all_instruments(type=[stype])
            # print(f"{stype} instruments count={len(instruments)}")

            for idx,inst in instruments.iterrows():
                # 获取每个最新发布的合约的建议时间段
                if ('trading_hours' not in inst) or not(isinstance(inst.trading_hours,str)):
                    # 跳过没有交易时间段或者交易时间段无效的合约
                    continue

                inst_name = inst.trading_code if stype == 'Future' else inst.order_book_id 
                inst_name = inst_name.upper() 
                if inst_name.find('.') < 0:
                    inst_name += '.' + inst.exchange

                if inst_name not in ths_dict:
                    str_trading_hours = inst.trading_hours

                    # 把'01-'或'31-'者替换成'00-'或'30-'
                    suffix_pair = [('1-','0-'),('6-','5-')]
                    for s1,s2 in suffix_pair:
                        str_trading_hours = str_trading_hours.replace(s1,s2)

                    # 如果原来没有,提取出来
                    trade_hours[inst_name] = {"name": inst.symbol,"trading_hours": str_trading_hours}

        # print(f"trade_hours old count {len(ths_dict)},append count={len(trade_hours)}")
        if trade_hours:
            ths_dict.update(trade_hours)
            save_json(self.trading_hours_file,ths_dict)

        return True

    def load_all_trading_hours(self) -> dict:       # hxxjava add 
        """ 从trading_hours.json文件中读取所有合约的交易时间段 """
        json_file = get_file_path(self.trading_hours_file)
        if not json_file.exists():
            return {}
        else:
            return load_json(self.trading_hours_file)

3.4.3 为main_engine安装一个可以获取交易时间段信息的接口

在vnpy\trader\engine.py中:

  • 该文件的引用部分:
from .datafeed import get_datafeed                  # hxxjava add
  • 为MainEngine类增加下面函数

    def get_trading_hours(self,vt_symbol:str) -> str:   # hxxjava add
        """ get vt_symbol's trading hours """
        ths = self.all_trading_hours.get(vt_symbol.upper(),"")       
        return ths["trading_hours"] if ths else ""

为什么在MainEngine类增加可以获取交易时间段信息的接口?

因为无论你运行vnpy中的哪个app,你都会启动main_engine,无需绕弯子就可以得到这些信息,而我们的用户策略中都包含各自策略的引擎,这样就方便获取交易时间段信息。

如CTA策略中包含cta_engine,而cta_engine它的成员就包含main_engine。那么在策略中执行类似下面的语句就可以获取您交易品种的交易时间段信息:

       trading_hours = self.cta_engine.main_engine.get_trading_hours(selt.vt_symbol)

如PortFolioStrategy策略中包含strategy_engine,而strategy_engine它的成员就包含main_engine。那么在策略中执行类似下面的语句就可以获取多个交易品种的交易时间段信息:

       trading_hours_list = [self.cta_engine.main_engine.get_trading_hours(vt_symbol) for vt_symbol in self.vt_symbols]

是不是很方便呢?

3.4.4 在系统的投研中执行更新所有品种(含期货、股票、指数和基金)的交易时间段

vnpy 3.0的启动界面中已经集成了一个叫“投研”的功能,其实它是jupyter lab,启动之后输入下面的代码:

# 测试update_all_trading_hours()函数和load_all_trading_hours()
from vnpy.trader.datafeed import get_datafeed

df = get_datafeed()
df.init()

df.update_all_trading_hours()   # 更新所有合约的交易时间段到本地文件中

ths = df.load_all_trading_hours() # 从本地文件中读取所有合约的交易时间段

当然您可以在vnpy的trader中主界面的菜单中增加一项,方便您在需要的时候执行下面语句。不过这个更新交易时间段的功能并不需要频繁执行,手动也就够了,记得就好。

3.4.5 你还可以手工打开trading_hours.json,直接输入

经过上面步骤3.4.4,您就在本地得到了一个trading_hours.json文件,该文件在您的用户目录下的.vntrader\中,其内容如下:

{
    "A0303.DCE": {
        "name": "豆一0303",
        "trading_hours": "21:00-23:00,09:00-10:15,10:30-11:30,13:30-15:00"
    },
    "A0305.DCE": {
        "name": "豆一0305",
        "trading_hours": "21:00-23:00,09:00-10:15,10:30-11:30,13:30-15:00"
    },
    "A0307.DCE": {
        "name": "豆一0307",
        "trading_hours": "21:00-23:00,09:00-10:15,10:30-11:30,13:30-15:00"
    },
    "A0309.DCE": {
        "name": "豆一0309",
        "trading_hours": "21:00-23:00,09:00-10:15,10:30-11:30,13:30-15:00"
    },
    "A0311.DCE": {
        "name": "豆一0311",
        "trading_hours": "21:00-23:00,09:00-10:15,10:30-11:30,13:30-15:00"
    },
    "A0401.DCE": {
        "name": "豆一0401",
        "trading_hours": "21:00-23:00,09:00-10:15,10:30-11:30,13:30-15:00"
    },
   ... ...
}

观察其格式,在你没有米筐数据接口或者这里没有的合约,您也可以手动输入合约交易时间段信息。

按照程序中算法,这个文件文件中一共包含约16500多个合约的交易时间段信息。可以覆盖国内金融市场几乎全部都产品,但是不包括金融二次衍生品期权。
为什么没有期权交易时间段信息,因为不需要。期权合约有其对应的标的物,从其名称和编号就可以解析出来。期权合约的交易时间段其和标的物的交易时间段是完全相同的,因此不需要保存到该文件中。

1. 系统自带的BarGenerator产生n分钟bar会丢弃一部分bar。

上周升级到了vnpy 2.9.0版本,编写了个策略,用到了30分钟Bar。

        self.dir_bg = BarGenerator(on_bar = self.on_bar,window = 30,
                                    on_window_bar = self.on_30m_bar,interval = Interval.MINUTE)

那个意思就是创建一个30分钟bar合成器。
策略的on_30m_bar()是这样的,先打印出来看看:

    def on_30m_bar(self, bar: BarData):
        """  
        收到方向周期的K线
        """
        print(f"{self.strategy_name}收到30分钟周期K线{bar}")

结果杯具了:

GsjyDemo2收到30分钟周期K线BarData(gateway_name='RQ', symbol='rb2205', exchange=<Exchange.SHFE: 'SHFE'>, datetime=datetime.datetime(2022, 2, 23, 21, 0, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>), interval=None, volume=225506.0, turnover=10790057010.0, open_interest=1921172.0, open_price=4788.0, high_price=4808.0, low_price=4762.0, close_price=4777.0)
GsjyDemo2收到30分钟周期K线BarData(gateway_name='RQ', symbol='rb2205', exchange=<Exchange.SHFE: 'SHFE'>, datetime=datetime.datetime(2022, 2, 23, 21, 30, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>), interval=None, volume=179987.0, turnover=8570390020.0, open_interest=1903437.0, open_price=4778.0, high_price=4778.0, low_price=4751.0, close_price=4760.0)
GsjyDemo2收到30分钟周期K线BarData(gateway_name='RQ', symbol='rb2205', exchange=<Exchange.SHFE: 'SHFE'>, datetime=datetime.datetime(2022, 2, 23, 22, 0, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>), interval=None, volume=99381.0, turnover=4723786710.0, open_interest=1905948.0, open_price=4760.0, high_price=4766.0, low_price=4743.0, close_price=4746.0)
GsjyDemo2收到30分钟周期K线BarData(gateway_name='RQ', symbol='rb2205', exchange=<Exchange.SHFE: 'SHFE'>, datetime=datetime.datetime(2022, 2, 23, 22, 30, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>), interval=None, volume=83782.0, turnover=3985661600.0, open_interest=1904511.0, open_price=4745.0, high_price=4767.0, low_price=4744.0, close_price=4763.0)
GsjyDemo2收到30分钟周期K线BarData(gateway_name='RQ', symbol='rb2205', exchange=<Exchange.SHFE: 'SHFE'>, datetime=datetime.datetime(2022, 2, 24, 9, 0, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>), interval=None, volume=99253.0, turnover=4714720470.0, open_interest=1916969.0, open_price=4763.0, high_price=4766.0, low_price=4738.0, close_price=4748.0)
GsjyDemo2收到30分钟周期K线BarData(gateway_name='RQ', symbol='rb2205', exchange=<Exchange.SHFE: 'SHFE'>, datetime=datetime.datetime(2022, 2, 24, 9, 30, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>), interval=None, volume=85886.0, turnover=4076563050.0, open_interest=1930796.0, open_price=4750.0, high_price=4760.0, low_price=4735.0, close_price=4736.0)
GsjyDemo2收到30分钟周期K线BarData(gateway_name='RQ', symbol='rb2205', exchange=<Exchange.SHFE: 'SHFE'>, datetime=datetime.datetime(2022, 2, 24, 10, 0, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>), interval=None, volume=491666.0, turnover=23080991050.0, open_interest=1982231.0, open_price=4736.0, high_price=4741.0, low_price=4660.0, close_price=4665.0)
GsjyDemo2收到30分钟周期K线BarData(gateway_name='RQ', symbol='rb2205', exchange=<Exchange.SHFE: 'SHFE'>, datetime=datetime.datetime(2022, 2, 24, 11, 0, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>), interval=None, volume=279000.0, turnover=13059711390.0, open_interest=2005223.0, open_price=4666.0, high_price=4713.0, low_price=4654.0, close_price=4676.0)
GsjyDemo2收到30分钟周期K线BarData(gateway_name='RQ', symbol='rb2205', exchange=<Exchange.SHFE: 'SHFE'>, datetime=datetime.datetime(2022, 2, 24, 13, 30, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>), interval=None, volume=271595.0, turnover=12701729900.0, open_interest=2021599.0, open_price=4664.0, high_price=4709.0, low_price=4648.0, close_price=4673.0)
GsjyDemo2收到30分钟周期K线BarData(gateway_name='RQ', symbol='rb2205', exchange=<Exchange.SHFE: 'SHFE'>, datetime=datetime.datetime(2022, 2, 24, 14, 0, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>), interval=None, volume=266018.0, turnover=12358486960.0, open_interest=2082998.0, open_price=4673.0, high_price=4674.0, low_price=4622.0, close_price=4623.0)
GsjyDemo2收到30分钟周期K线BarData(gateway_name='RQ', symbol='rb2205', exchange=<Exchange.SHFE: 'SHFE'>, datetime=datetime.datetime(2022, 2, 24, 14, 30, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>), interval=None, volume=175873.0, turnover=8162467860.0, open_interest=2081475.0, open_price=4624.0, high_price=4654.0, low_price=4624.0, close_price=4637.0)
GsjyDemo2收到30分钟周期K线BarData(gateway_name='RQ', symbol='rb2205', exchange=<Exchange.SHFE: 'SHFE'>, datetime=datetime.datetime(2022, 2, 24, 21, 0, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>), interval=None, volume=312119.0, turnover=14487421790.0, open_interest=2043795.0, open_price=4635.0, high_price=4664.0, low_price=4613.0, close_price=4655.0)
GsjyDemo2收到30分钟周期K线BarData(gateway_name='RQ', symbol='rb2205', exchange=<Exchange.SHFE: 'SHFE'>, datetime=datetime.datetime(2022, 2, 24, 21, 30, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>), interval=None, volume=106717.0, turnover=4970359120.0, open_interest=2025130.0, open_price=4656.0, high_price=4666.0, low_price=4648.0, close_price=4655.0)
GsjyDemo2收到30分钟周期K线BarData(gateway_name='RQ', symbol='rb2205', exchange=<Exchange.SHFE: 'SHFE'>, datetime=datetime.datetime(2022, 2, 24, 22, 0, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>), interval=None, volume=60255.0, turnover=2807077210.0, open_interest=2011503.0, open_price=4657.0, high_price=4665.0, low_price=4652.0, close_price=4659.0)
GsjyDemo2收到30分钟周期K线BarData(gateway_name='RQ', symbol='rb2205', exchange=<Exchange.SHFE: 'SHFE'>, datetime=datetime.datetime(2022, 2, 24, 22, 30, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>), interval=None, volume=184047.0, turnover=8524883500.0, open_interest=1989335.0, open_price=4660.0, high_price=4661.0, low_price=4608.0, close_price=4614.0)

错误:居然把每天10:00-11:00的1分钟bar合成为1个30分钟bar !无论怎样也是错误的,因为10:00-11:00一共有45分钟到交易数据,怎么也不能合成为1个30分钟bar,肯定错误了。

2. 错在哪里了?

找到BarGenerator的错误了:

    def update_bar_minute_window(self, bar: BarData) -> None:
        """"""
        # If not inited, create window bar object
        if not self.window_bar:
            dt = bar.datetime.replace(second=0, microsecond=0)
            self.window_bar = BarData(
                symbol=bar.symbol,
                exchange=bar.exchange,
                datetime=dt,
                gateway_name=bar.gateway_name,
                open_price=bar.open_price,
                high_price=bar.high_price,
                low_price=bar.low_price
            )
        # Otherwise, update high/low price into window bar
        else:
            self.window_bar.high_price = max(
                self.window_bar.high_price,
                bar.high_price
            )
            self.window_bar.low_price = min(
                self.window_bar.low_price,
                bar.low_price
            )

        # Update close price/volume/turnover into window bar
        self.window_bar.close_price = bar.close_price
        self.window_bar.volume += bar.volume
        self.window_bar.turnover += bar.turnover
        self.window_bar.open_interest = bar.open_interest

        Check if window bar completed

        # 这里错误了,用当前1分钟到分钟数+1与30取模来决定一个30分钟K线是否结束,
        # 先推送已合成bar,在生成下一个新的30分钟bar。
        # 可是10:15-10:30是休市时间段,永远也等不到10:29分钟到那个1分钟bar,所以只能在10:59符合条件,
        # 因此这个30分钟bar实际上包含了45分钟到交易数据,错误!!!

        if not (bar.datetime.minute + 1) % self.window:   
             self.on_window_bar(self.window_bar)
             self.window_bar = None

3. 如何解决?

问题分析清楚了,就不再解释怎么修改了,直接上修改的BarGenerator完整代码吧。
BarGenerator在vnpy.trader.utility中,拷贝过去替换就OK了。
测试过了,和文华6产生的30分钟K线一模一样。
如果想知道哪里修改了,查找 # hxxjava就可以找到修改处。

3.1 修改的代码

vnpy/trader/utility.py的前面添加引用:

from datetime import timedelta

BarGenerator的修改如下:

class BarGenerator:
    """
    For:
    1. generating 1 minute bar data from tick data
    2. generating x minute bar/x hour bar data from 1 minute data

    Notice:
    1. for x minute bar, x must be able to divide 60: 2, 3, 5, 6, 10, 15, 20, 30
    2. for x hour bar, x can be any number
    """

    def __init__(
        self,
        on_bar: Callable,
        window: int = 0,
        on_window_bar: Callable = None,
        interval: Interval = Interval.MINUTE,
        daily_close_time:str = "15:00"
    ):
        """Constructor"""
        self.bar: BarData = None
        self.on_bar: Callable = on_bar

        self.interval: Interval = interval
        self.interval_count: int = 0

        self.hour_bar: BarData = None

        self.window: int = window
        self.count_for_window : int = 0   # hxxjava add
        self.window_bar: BarData = None
        self.on_window_bar: Callable = on_window_bar

        self.last_tick: TickData = None
        self.daily_close_time = daily_close_time

    def update_tick(self, tick: TickData) -> None:
        """
        Update new tick data into generator.
        """
        new_minute = False

        # Filter tick data with 0 last price
        if not tick.last_price:
            return

        # Filter tick data with older timestamp
        if self.last_tick and tick.datetime < self.last_tick.datetime:
            return

        if not self.bar:
            new_minute = True
        elif (
            (self.bar.datetime.minute != tick.datetime.minute)
            or (self.bar.datetime.hour != tick.datetime.hour)
        ):
            self.bar.datetime = self.bar.datetime.replace(
                second=0, microsecond=0
            )
            self.on_bar(self.bar)

            new_minute = True

        if new_minute:
            self.bar = BarData(
                symbol=tick.symbol,
                exchange=tick.exchange,
                interval=Interval.MINUTE,
                datetime=tick.datetime,
                gateway_name=tick.gateway_name,
                open_price=tick.last_price,
                high_price=tick.last_price,
                low_price=tick.last_price,
                close_price=tick.last_price,
                open_interest=tick.open_interest
            )
        else:
            self.bar.high_price = max(self.bar.high_price, tick.last_price)
            if tick.high_price > self.last_tick.high_price:
                self.bar.high_price = max(self.bar.high_price, tick.high_price)

            self.bar.low_price = min(self.bar.low_price, tick.last_price)
            if tick.low_price < self.last_tick.low_price:
                self.bar.low_price = min(self.bar.low_price, tick.low_price)

            self.bar.close_price = tick.last_price
            self.bar.open_interest = tick.open_interest
            self.bar.datetime = tick.datetime

        if self.last_tick:
            volume_change = tick.volume - self.last_tick.volume
            self.bar.volume += max(volume_change, 0)

            turnover_change = tick.turnover - self.last_tick.turnover
            self.bar.turnover += max(turnover_change, 0)

        self.last_tick = tick

    def update_bar(self, bar: BarData) -> None:
        """
        Update 1 minute bar into generator
        """
        if self.interval == Interval.MINUTE:
            self.update_bar_minute_window(bar)
        else:
            self.update_bar_hour_window(bar)

    def update_bar_minute_window(self, bar: BarData) -> None:
        """"""
        # If not inited, create window bar object
        if not self.window_bar:
            dt = bar.datetime.replace(second=0, microsecond=0)
            self.window_bar = BarData(
                symbol=bar.symbol,
                exchange=bar.exchange,
                datetime=dt,
                gateway_name=bar.gateway_name,
                open_price=bar.open_price,
                high_price=bar.high_price,
                low_price=bar.low_price
            )
        # Otherwise, update high/low price into window bar
        else:
            self.window_bar.high_price = max(
                self.window_bar.high_price,
                bar.high_price
            )
            self.window_bar.low_price = min(
                self.window_bar.low_price,
                bar.low_price
            )

        # Update close price/volume/turnover into window bar
        self.window_bar.close_price = bar.close_price
        self.window_bar.volume += bar.volume
        self.window_bar.turnover += bar.turnover
        self.window_bar.open_interest = bar.open_interest

        # Check if window bar completed
        # if not (bar.datetime.minute + 1) % self.window:
        #     self.on_window_bar(self.window_bar)
        #     self.window_bar = None

        # hxxjava add start
        h,m = self.daily_close_time.split(':')
        today_close_time = bar.datetime.replace(hour=int(h),minute=int(m),second=0,microsecond=0)
        enter_next_day = bar.datetime + timedelta(minutes=1) == today_close_time
        if self.count_for_window + 1 == self.window or enter_next_day:
            self.on_window_bar(self.window_bar)
            self.window_bar = None

        if enter_next_day:
            self.count_for_window = 0
        else:
            self.count_for_window += 1
            self.count_for_window %= self.window

        # hxxjava add end

    def update_bar_hour_window(self, bar: BarData) -> None:
        """"""
        # If not inited, create window bar object
        if not self.hour_bar:
            dt = bar.datetime.replace(minute=0, second=0, microsecond=0)
            self.hour_bar = BarData(
                symbol=bar.symbol,
                exchange=bar.exchange,
                datetime=dt,
                gateway_name=bar.gateway_name,
                open_price=bar.open_price,
                high_price=bar.high_price,
                low_price=bar.low_price,
                close_price=bar.close_price,
                volume=bar.volume,
                turnover=bar.turnover,
                open_interest=bar.open_interest
            )
            return

        finished_bar = None

        # If minute is 59, update minute bar into window bar and push
        if bar.datetime.minute == 59:
            self.hour_bar.high_price = max(
                self.hour_bar.high_price,
                bar.high_price
            )
            self.hour_bar.low_price = min(
                self.hour_bar.low_price,
                bar.low_price
            )

            self.hour_bar.close_price = bar.close_price
            self.hour_bar.volume += bar.volume
            self.hour_bar.turnover += bar.turnover
            self.hour_bar.open_interest = bar.open_interest

            finished_bar = self.hour_bar
            self.hour_bar = None

        # If minute bar of new hour, then push existing window bar
        elif bar.datetime.hour != self.hour_bar.datetime.hour:
            finished_bar = self.hour_bar

            dt = bar.datetime.replace(minute=0, second=0, microsecond=0)
            self.hour_bar = BarData(
                symbol=bar.symbol,
                exchange=bar.exchange,
                datetime=dt,
                gateway_name=bar.gateway_name,
                open_price=bar.open_price,
                high_price=bar.high_price,
                low_price=bar.low_price,
                close_price=bar.close_price,
                volume=bar.volume,
                turnover=bar.turnover,
                open_interest=bar.open_interest
            )
        # Otherwise only update minute bar
        else:
            self.hour_bar.high_price = max(
                self.hour_bar.high_price,
                bar.high_price
            )
            self.hour_bar.low_price = min(
                self.hour_bar.low_price,
                bar.low_price
            )

            self.hour_bar.close_price = bar.close_price
            self.hour_bar.volume += bar.volume
            self.hour_bar.turnover += bar.turnover
            self.hour_bar.open_interest = bar.open_interest

        # Push finished window bar
        if finished_bar:
            self.on_hour_bar(finished_bar)

    def on_hour_bar(self, bar: BarData) -> None:
        """"""
        if self.window == 1:
            self.on_window_bar(bar)
        else:
            if not self.window_bar:
                self.window_bar = BarData(
                    symbol=bar.symbol,
                    exchange=bar.exchange,
                    datetime=bar.datetime,
                    gateway_name=bar.gateway_name,
                    open_price=bar.open_price,
                    high_price=bar.high_price,
                    low_price=bar.low_price
                )
            else:
                self.window_bar.high_price = max(
                    self.window_bar.high_price,
                    bar.high_price
                )
                self.window_bar.low_price = min(
                    self.window_bar.low_price,
                    bar.low_price
                )

            self.window_bar.close_price = bar.close_price
            self.window_bar.volume += bar.volume
            self.window_bar.turnover += bar.turnover
            self.window_bar.open_interest = bar.open_interest

            self.interval_count += 1
            if not self.interval_count % self.window:
                self.interval_count = 0
                self.on_window_bar(self.window_bar)
                self.window_bar = None

    def generate(self) -> Optional[BarData]:
        """
        Generate the bar data and call callback immediately.
        """
        bar = self.bar

        if self.bar:
            bar.datetime = bar.datetime.replace(second=0, microsecond=0)
            self.on_bar(bar)

        self.bar = None
        return bar

3.2 修改说明

本人修改原则是n分钟bar按照日内对齐的原则,即:

  • 跨过日内的休市时间段连续合成;
  • n分钟生成一个bar并且立即生成一个bar并且推送之;
  • 但是遇到收市,无论是否满n分钟并且立即生成一个bar并且推送之。

3.3 使用说明

注意到BarGenerator的构造函数多了个daily_close_time参数,字符串类型,默认值为"15:00"。
例如:

  • 如果是国内股票、期货或期权,大部分交易品种都无需特别修改收市时间,忽略即可;例如:
self.bg30m = BarGenerator(on_bar = self.on_bar,window = 30,on_window_bar = self.on_30m_bar,interval = Interval.MINUTE)  # 默认15:00收市

但是如果有些例如国债等品种,它的收市时间不是15:00,则需要在特别传参,在写作交易策略的时候,可以给出代表收市时间的字符串参数,供创建实例的时候传递给该参数。虽然麻烦了一丢丢,但是已经可以算得上是够方便的啦!

例如:

  • 如果交易的是每日16:00收市的品种,这样创建30分钟bar生成器:
self.bg30m = BarGenerator(on_bar = self.on_bar,window = 30,
                                    on_window_bar = self.on_30m_bar,
                                    interval = Interval.MINUTE,
                                    daily_close_time= "16:00" )
  • 如果交易的是每日伦敦金,每日收市是北京时间5:00,这样创建30分钟bar生成器:
self.bg30m = BarGenerator(on_bar = self.on_bar,window = 30,
                                    on_window_bar = self.on_30m_bar,
                                    interval = Interval.MINUTE,
                                    daily_close_time= "5:00" )

(贴子标题不小心打错字了,无法修改 ,抱歉!!!)

准备工作

安装Markdown插件

  • Markdown All in One
  • Markdown Paste
  • Markdown PDF
  • Markdown Preview Enhanced
  • Markdown Preview Mermaid Support

description

工作区界面

description

markdown文档的编辑和预览联动示意

description

相关的快捷键

  • Shift+Alt+F :格式化文档
  • Ctrl+Alt+V :Markdown Paste
  • Ctrl+Alt+C :Markdown Paste Code
  • Ctrl+K V :MPE:打开侧边预览
  • Ctrl+Shift+P :命令面板

鼠标右键菜单命令

  • Markdown PDF: Export (settings.json)
  • Markdown PDF: Export (pdf)
  • Markdown PDF: Export (html)
  • Markdown PDF: Export (png)
  • Markdown PDF: Export (jipeg)
  • Markdown PDF: Export (all: pdf, html, png, jpeg)

下面是一个用VSCode编写Markdown的演示文档

创建markdown文档

首先用VSCode创建新文档,把它保存为扩展名为md的文档,这就表示它是一个markdown文档了。

说明:

  • 虽然vn.py官网支持markdown语法,但是支持的还不够全面(也许是咱不熟悉),我编写的演示文档有许多地方还是不可在vn.py官网,所以我把不可以直接被显示的地方贴在显示效果之前,您可以把这些代码复制到VSCode中,在markdown相关插件齐全的情况下,是可以成功显示的。
  • 例如后面Tek数学公式可mermaid绘图语法的演示,分成实现代码和显示效果,您可以复制到VSCode中,参考示例的写法,修改成符合自己要求的各种图。

基本markdown语法的使用

无序列表

  • 一月份
  • 二月份
  • 三月份
  • 四月份

第二级标题

标题的写法:Markdown标题由 ‘#’ 开始:

# —— 一级标题
## —— 二级标题
### —— 三级标题
#### —— 四级标题
##### —— 五级标题
###### —— 六级标题

其中侧边栏上半部分显示的是当前工作区文件夹下的文件,包括 Markdown 文件和素材。下半部分可以展开「Outline」视图,即大纲,可以根据当前正在编辑的 Markdown 文件显示其各级标题的大纲目录。

这里是引用演示:

> 1st reference
Markdown 文件和素材。下半部分可以展开「Outline」视图,即大纲,可以根据当前正在编辑的 Markdown 文件显示其各级标题的大纲目录。
>> 2nd reference
Markdown 文件和素材。下半部分可以展开「Outline」视图,即大纲,可以根据当前正在编辑的 Markdown 文件显示其各级标题的大纲目录。
>>> third level reference
Markdown 文件和素材。下半部分可以展开「Outline」视图,即大纲,可以根据当前正在编辑的 Markdown 文件显示其各级标题的大纲目录。

显示效果:

1st reference
Markdown 文件和素材。下半部分可以展开「Outline」视图,即大纲,可以根据当前正在编辑的 Markdown 文件显示其各级标题的大纲目录。

2nd reference
Markdown 文件和素材。下半部分可以展开「Outline」视图,即大纲,可以根据当前正在编辑的 Markdown 文件显示其各级标题的大纲目录。

third level reference
Markdown 文件和素材。下半部分可以展开「Outline」视图,即大纲,可以根据当前正在编辑的 Markdown 文件显示其各级标题的大纲目录。

下面是python代码示例:

python
import time

# Quick,count to ten!for i in range( 10):
# ( but not *too* quick)time.sleep(0.5)

for i in range(1,100):
    print(i)

这是行内的代码:x = this.count,它是嵌入到文字中的。

第三级标题

无序列表

  • 侧边栏
  • 编辑区域
  • 预览区域

有序列表

  1. 其中侧边栏上半部分显示的是当
  2. 前工作区文件夹下的文件,
  3. 包括 Markdown 文件和素材。
  4. 下半部分可以展开「Outline」
  5. 视图,即大纲,可以根

字体修饰(加重、斜体、删除)

加重显示 值得一提的是斜体字, VS Code 的 Markdown 预览默认渲染的是当前你正在编辑的文档,不过如果你希望将某个文档的预览渲染锁定不动,可以通过「Markdown: Toggle Preview Locking」调出一个锁定某个文档的预览界面。

LaTex 数学表达式

···
InLine math equations:$\omega = d\phi / dt$. Display math should get its own line like so:

$$I = \int \rho R^{2} dV$$
$$ 2+3 = 5 $$
$x+y=z^2$
$ax+by=cz$
$2x+3y=4z$
···
显示:

description

表格示例

  • 表格示例1
    ···
    | 源文件 | 和预览界面 | 是同步的 |
    | :----: | :--------: | :------:|
    | 即你无 | 论是在编辑 | 界面还是 |
    | 即你无 | 论是在编辑 | 界面还是 |
    | 即你无 | 论是在编辑 | 界面还是 |
    ···

description

  • 表格示例2————表格还可以如此方便
    ···
    title | 标题
    --:|:--
    dateFormat |日期格式
    section |模块
    Completed | 已经完成
    Active | 当前正在进行
    Future | 后续待处理
    crit | 关键阶段
    日期缺失 | 默认从上一项完成后
    ···
    显示:

description

图片示例

用代码插入图片示例 !

![test.jpg](images/test.jpg)

显示:

description

剪贴板图片插入示例 !

  1. 先找到自己需要的界面,将图形复制到剪贴板
  2. 在VSCode中编辑的md文档需要插入图片的位置,点击鼠标右键,选择“Markdown Paste”或者Ctrl+Alt+V快捷键,进入下面的图片命名环节,可以默认,也可以另外输入新名称。
  3. 完成上面的两步后,md文档就被自动添加了类似下面的语句:![图片插入.png](图片插入.png),当然它和手工输入是同样的显示效果。

description

超链接示例

百度搜索

  • apple
  • carrot
  • pear

下面是一些非常炫酷的Mermaid图

序列图示例1:

```mermaid
sequenceDiagram
    participant Alice
    participant Bob
    Alice->>John: Hello John,how are you?
    loop Healthcheck
        John->>John: Fight against hypochondria
    end
    Note right of John: Rational thoughts<br/>prevail...
    John-->>Alice: Great!
    John->>Bob: How about you?
    Bob-->>John: Jolly good!|
    John-->Alice:我爱你!
```

显示:

description


序列图示例2:

```mermaid
  sequenceDiagram
    Note right of A: 倒霉, 碰到B了
    A->B:   Hello B, how are you ?
    note left of B: 倒霉,碰到A了
    B-->A:  Fine, thx, and you?
    note over A,B: 快点溜,太麻烦了
    A->>B:  I'm fine too.
    note left of B: 快点打发了A
    B-->>A: Great!
    note right of A: 溜之大吉
    A-xB:   Wait a moment
    loop Look B every minute
      A->>B: look B, go?
      B->>A: let me go?
    end
    B--xA: I'm off, byte
    note right of A: 太好了, 他走了
```

description


序列图示例3:

```mermaid
  sequenceDiagram
    Alice->>Bob: Hello Bob, how are you?
  alt is sick
    Bob->>Alice:not so good :(
  else is well
    Bob->>Alice:good
  end
  opt Extra response
    Bob->>Alice:Thanks for asking
  end
```

description


序列图示例4:

```mermaid
sequenceDiagram
  # 通过设定参与者(participant)的顺序控制展示顺序
  participant B
  participant A
  Note right of A: 倒霉, 碰到B了
  A->B:   Hello B, how are you ?
  note left of B: 倒霉,碰到A了
  B-->A:  Fine, thx, and you?
  note over A,B:快点溜,太麻烦了。。。
  A->>B:  I'm fine too.
  note left of B: 快点打发了A
  B-->>A: Great!
  note right of A: 溜之大吉
  A-xB:   Wait a moment
  loop Look B every minute
    A->>B: look B, go?
    B->>A: let me go?
  end
  B--xA: I'm off, byte
  note right of A: 太好了, 他走了
```

description


序列图示例5:

```mermaid
  sequenceDiagram 
    # 通过设定参与者(participants)的顺序控制展示模块顺序
    participant Alice
    participant Bob 
    participant John 
    Alice->John:Hello John,how are you?
    loop Healthcheck
      John->John:Fight against hypochondria
    end
    Note right of John:Rational thoughts <br/>prevail...  John-->Alice:Great! 
    John->Bob: How about you?
    Bob-->John: good!
```

description


甘特图示例

甘特图示例1:

```mermaid
  gantt
    dateFormat YYYY-MM-DD
    section S1
    T1: 2014-01-01, 9d
    section S2
    T2: 2014-01-11, 9d
    section S3
    T3: 2014-01-02, 9d
```

description

甘特图示例2:

```mermaid
  gantt
    dateFormat  YYYY-MM-DD
    title Adding GANTT diagram functionality to mermaid

    section A section
    Completed task            :done,    des1, 2014-01-06,2014-01-08
    Active task               :active,  des2, 2014-01-09, 3d
    Future task               :         des3, after des2, 5d
    Future task2               :         des4, after des3, 5d

    section Critical tasks
    Completed task in the critical line :crit, done, 2014-01-06,24h
    Implement parser and jison          :crit, done, after des1, 2d
    Create tests for parser             :crit, active, 3d
    Future task in critical line        :crit, 5d
    Create tests for renderer           :2d
    Add to mermaid                      :1d

    section Documentation
    Describe gantt syntax               :active, a1, after des1, 3d
    Add gantt diagram to demo page      :after a1  , 20h
    Add another diagram to demo page    :doc1, after a1  , 48h

    section Last section
    Describe gantt syntax               :after doc1, 3d
    Add gantt diagram to demo page      : 20h
    Add another diagram to demo page    : 48h
```

description


类图示例:

```mermaid
  classDiagram
    Class01 <|-- AveryLongClass : Cool    
    Class03 *-- Class04
    Class05 o-- Class06
    Class07 .. Class08
    Class09 --> C2 : Where am i?
    Class09 --* C3
    Class09 --|> Class07
    Class07 : equals()
    Class07 : Object[] elementData
    Class01 : size()
    Class01 : int chimp
    Class01 : int gorilla
    Class08 <--> C2: Cool label    
```

description

饼图示例:

```mermaid
  pie
    "Dogs" : 386
    "Cats" : 85
    "Rats" : 15
```

description


实体关系图示例

```mermaid
erDiagram
    CUSTOMER ||--o{ ORDER : places
    ORDER ||--|{ LINE-ITEM : contains
    CUSTOMER }|..|{ DELIVERY-ADDRESS : uses
```

description


旅游图示例:

```mermaid
  journey
      title My working day
      section Go to work
        Make tea: 5: Me
        Go upstairs: 3: Me
        Do work: 1: Me, Cat
      section Go home
        Go downstairs: 5: Me
        Sit down: 5: Me
```

description

总结

本文给您介绍了这些文档元素:

  • 各级标题(1~6级)
  • 文本修饰(字体、颜色、加粗、斜体、删除线...)
  • 无序列表
  • 有序列表
  • 程序代码(行内和独立)
  • 引用
  • LaTex数学表达式(行内和独立)
  • 表格(包括对齐方式)
  • 图片(手工插入和自动插入)
  • 超链接
  • 各种Mermaid图
    • 序列图
    • 流程图
    • 甘特图
    • 类图
    • 饼图
    • 实体
    • 旅游图

掌握了上述的Markdown语法,您可以轻松编写自己的Markdown文档了!

1. save_bar_data()函数的功能

  • 保存一个bar列表bars到表DbBarData中,对已经存在相同索引的bar就更新之
  • 用bars的第一个bar和最后一个bar的时间连同symbol、exchange和interval,计算出数据总揽overview,
  • 果DbBarOverview已经存在一个同symbol、exchange和interval的数据总揽overview0,那么就合并overview0的开始时间start和end,合并的规则如下:
              overview.start = min(bars[0].datetime, overview.start)
              overview.end = max(bars[-1].datetime, overview.end)

2. save_bar_data()生成的数据总揽的特点

它会把DbBarData中相同symbol、exchange和interval的所有bar的生成一个数据总揽overview,
并且overview的起止时间分别为这些bar最早时间戳和最晚时间戳——无论这些bars是由几个时间段构成的。
也就是说,如果您曾经下载过过螺纹rb2201.SHFE的日线数据:

  • 第一次:2021-1-16~2021-2-16 —— BarOverview为 rb2201.SHFE interval='d' 起止时间:2021-1-16~2021-2-16
  • 第二次:2021-2-16~2021-5-16 —— BarOverview为 rb2201.SHFE interval='d' 起止时间:2021-1-16~2021-5-16
  • 第三次:2021-12-1~2021-12-22 —— BarOverview为 rb2201.SHFE interval='d' 起止时间:2021-1-16~2021-12-22

注意:

这三次下载后,rb2201.SHFE的日线数据的BarOverview的起止时间:2021-1-16~2021-12-22,而实际上数据库中是不存在2021-5-17~2021-11-30,除非我们查询明细,否则我们不知道它们还缺少哪些日期的K线数据!

3. 完全没有重叠的两次下载,不应该合并到一起

我觉得对于下载的时间段重叠的可以合并成为一个BarOverview,目前vnpy在这点上的处理就很好。
而对于两个没有任何重叠时间段的两段bars是不应该合并的,因为这会引起误导!仔细地研究下数据管理模块的界面,就会明白我的意思了。

1. 问题的发现

1.1 问题代码

在jupyter notebook中,我编写了下面的一段下载合约K线数据的代码:

from datetime import datetime,date,timedelta
from vnpy.trader.utility import extract_vt_symbol
from vnpy.trader.constant import Exchange,Interval
from vnpy.trader.object import HistoryRequest
from vnpy.trader.database import get_database
from vnpy.trader.datafeed import get_datafeed

df = get_datafeed()

end = datetime.now()
start = end.replace(month=11)
start,end

req = HistoryRequest(symbol='rb2201',exchange=Exchange.SHFE,start=start,end=end,interval=Interval.DAILY)

# 语句1
bars = df.query_bar_history(req)

# 语句2
db.save_bar_data(bars)

# 语句3
for bar in bars: 
    print(bar)

1.2 问题描述:

执行上面代码,当执行到语句3的时候,提示:

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-11-f72a667896f4> in <module>
      1 for bar in bars:
----> 2     print(bar)

D:\ProgramFiles\vnstudio\lib\dataclasses.py in __repr__(self)

AttributeError: 'BarData' object has no attribute 'gateway_name'

如果把语句2注释掉,执行语句3的时候有没有任何问题,可以把从米筐接口读取的rb2201.SHFE的bar全部打印出来。

2. 错误的解决方法

由于本人使用的是mysql数据库,所以需要对vnpy_mysql目录下的mysql_database.py进行修改,使用其他数据库的用户其实也有同样的问题。解决方法也是一样的,不再每个都说。
方法:
打开vnpy_mysql\mysql_database.py,在文件头部添加下面的语句:

import copy     # hxxjava

对下面两个函数进行如下的修改,save_bar_data()和save_tick_data()都有同样的问题,修改之处语句在函数中注明了,错误原因是在遍历保存bars和ticks的过程中,因为数据库的要求,改变了每个bar和tick的 __dict__ 字典,保存动作做完了以后,列表bars和列表ticks的每个bar和tick已经被改得面目全非了,里面的gateway_name已经没有了。
修改过的代码后面有 # hxxjava change注释标记。
修改后的代码如下:

    def save_bar_data(self, bars: List[BarData]) -> bool:
        """保存K线数据"""
        # 读取主键参数
        bar = bars[0]
        symbol = bar.symbol
        exchange = bar.exchange
        interval = bar.interval

        # 将BarData数据转换为字典,并调整时区
        data = []

        for bar in bars:                           
            bar.datetime = convert_tz(bar.datetime)

            d = copy.copy(bar.__dict__)     # hxxjava change
            d["exchange"] = d["exchange"].value
            d["interval"] = d["interval"].value
            d.pop("gateway_name")
            d.pop("vt_symbol")
            data.append(d)

        # 使用upsert操作将数据更新到数据库中
        with self.db.atomic():
            for c in chunked(data, 50):
                DbBarData.insert_many(c).on_conflict_replace().execute()

        # 更新K线汇总数据
        overview: DbBarOverview = DbBarOverview.get_or_none(
            DbBarOverview.symbol == symbol,
            DbBarOverview.exchange == exchange.value,
            DbBarOverview.interval == interval.value,
        )

        if not overview:
            overview = DbBarOverview()
            overview.symbol = symbol
            overview.exchange = exchange.value
            overview.interval = interval.value
            overview.start = bars[0].datetime
            overview.end = bars[-1].datetime
            overview.count = len(bars)
        else:
            overview.start = min(bars[0].datetime, overview.start)
            overview.end = max(bars[-1].datetime, overview.end)

            s: ModelSelect = DbBarData.select().where(
                (DbBarData.symbol == symbol)
                & (DbBarData.exchange == exchange.value)
                & (DbBarData.interval == interval.value)
            )
            overview.count = s.count()

        overview.save()

        return True

    def save_tick_data(self, ticks: List[TickData]) -> bool:
        """保存TICK数据"""
        # 将TickData数据转换为字典,并调整时区
        data = []

        for tick in ticks:            
            tick.datetime = convert_tz(tick.datetime)

            d = copy.copy(tick.__dict__) # hxxjava change
            d["exchange"] = d["exchange"].value
            d.pop("gateway_name")
            d.pop("vt_symbol")
            data.append(d)

        # 使用upsert操作将数据更新到数据库中
        with self.db.atomic():
            for c in chunked(data, 50):
                DbTickData.insert_many(c).on_conflict_replace().execute()

        return True

1. 当前vnpy的CTP网关没有考虑CTP流控

1.1 什么是CTP流控?

详细见 报单流控、查询流控和会话数控制,这里不再赘述,有兴趣的读者可以去看看。

1.2 目前vnpy的ctp_gateway 不符合CTP流控

  1. 目前的vnpy的ctp_gateway完全看不到CTP接口的报单流控和查询流控的特点,只有一些API和SPI的函数实现
  2. 执行报单和查询相关操作时,网关应该有两个统一的队列来调度,以便满足CTP接口对流控的要求,而不是把流控留给上层的APP去处理
  3. 当上层app和ctp_gateway都不管报单流控,就会出现多个用户策略同时发出报单而超越流控限制现象,这在实际交易中经常出现,问题可能就在这里!

让我们看看下面的ctp_gateway的td_api.send_order()代码:

    def send_order(self, req: OrderRequest) -> str:
        """委托下单"""
       # .... 这里省略
        self.reqid += 1
        self.reqOrderInsert(ctp_req, self.reqid)            # 这里有问题

        orderid: str = f"{self.frontid}_{self.sessionid}_{self.order_ref}"
        order: OrderData = req.create_order_data(orderid, self.gateway_name)
        self.gateway.on_order(order)

        return order.vt_orderid

上面代码中self.reqOrderInsert(ctp_req, self.reqid)就没有考虑报单流控,它有4个返回值:
0,代表成功。
-1,表示网络连接失败;
-2,表示未处理请求超过许可数;
-3,表示每秒发送请求数超过许可数。
其中两个是有关流控的,如果是-2,-3我们是可以采用类似等待、重试、队列等方法的,可是都没有。
这导致的问题是如果是返回了-2,-3,那么,报单是没有成功的,上层app还以为已经成功了,也不会采用什么其它逻辑来补救。

1.3 不符合CTP流控,有什么问题?

 CTP交易接口一定的报单和查询必须符合CTP流控规则,默认FTD报文流控如果是每秒6次,报单和查询都会形成FTD报文,查询流控为1秒1次,报单流控最大是每秒6次,假如当前秒已经有了1次查询,那么报单的流控就只有5次了。
如果报单超越了当前的流控,那么报单坑定是失败的,交易服务器是不会接受报单请求的,无论你是开仓还是平仓,CTP交易接口根本没有成交的可能!
这样就造成我们以为发出去的符合条件成交条件委托是成功的,可是却是石沉大海,没有了下文。
实际的危害可能是:机会来了无法入场,灾难来临无法逃离 !

2. 如何改造当前的CTP网关?

改造的主要思路是:只要没有违反流控,直接快速执行报单请求和查询请求,如遇违反流控,让CTP网关缓冲报单请求和查询请求,延后执行。

  • 构造报单请求队列,用来缓存遇到流控问题的报单请求;
  • 构造查询请求队列,用来缓存遇到流控问题的查询请求;
  • 对于来自上层app的调用CTP交易接口中的 报单请求,保持与之前相同接口样式,立即执行。如果遇到返回值为流控错误,则将报单请求及参数压入报单请求队列;
  • 对于来自上层app的调用CTP交易接口中的 查询请求,保持与之前相同接口样式,立即执行。如果遇到返回值为流控错误,则将查询请求及参数压入查询请求队列;
  • 定时每1/6秒检查报单请求队列一次,从队列的头部取出报单请求并且执行。如果执行成功将该报单请求丢弃,如果遇到返回值为流控错误,再次将报单请求及参数压入报单请求队列;
  • 定时每1/6秒检查查询请求队列一次,从队列的头部取出查询请求并且执行。如果执行成功将该查询请求丢弃,如果遇到返回值为流控错误,再次将查询请求及参数压入查询请求队列。

3. 符合CTP流控的ctp_gateway 实现代码

本次分享的ctp_gateway比当前vnpy系统的ctp_gateway具备更为丰富的接口,其中包括:

  • 查询投资者
  • 查询投资单元
  • 查询经纪公司交易参数
  • 查询产品
  • 查询交易所保证金比率
  • 查询交易所保证金比率调整
  • 查询合约保证金比率
  • 查询手续费率
  • 查询报单手续费率
  • 合约交易状态推送
  • 交易所保证金比率推送

为了提高接口推送消息的能力,在ctp_gateway登录交易接口时,先连续不停地接收并解码合约状态、交易所保证金和合约信息这三个数量巨大的推送数据,然后分别一次性推送这些数据到系统的消息引擎中。这些消息接收端应该一次性地对这些消息进行处理。

3.1 修改vnpy\trader\constant,py,加入下面的代码:


class HedgeType(Enum):
    """
    投机/套保/备兑类型  hxxjava add
    """
    SEPCULATION = '1' #"投机"  
    ARBITRAGE = '2'   #"套利"   
    HEDGE = '3'       #"套保"     
    MARKETMAKER = '5' #"做市商"  
    SPECHEDGE = '6'   # 第一腿投机第二腿套保 大商所专用  
    HEDGESPEC = '7'   # 第一腿套保第二腿投机  大商所专用  


class InstrumentStatus(Enum):
    """
    合约交易状态类型 hxxjava debug
    """
    BEFORE_TRADING = "开盘前"
    NO_TRADING = "非交易"
    CONTINOUS = "连续交易" 
    AUCTION_ORDERING = "集合竞价报单"
    AUCTION_BALANCE = "集合竞价价格平衡"
    AUCTION_MATCH = "集合竞价撮合"
    CLOSE = "收盘"


# 有效交易状态
VALID_TRADE_STATUSES = [
    InstrumentStatus.CONTINOUS,
    InstrumentStatus.AUCTION_ORDERING,
    InstrumentStatus.AUCTION_BALANCE,
    InstrumentStatus.AUCTION_MATCH
]

# 集合竞价交易状态
AUCTION_STATUS = [
    InstrumentStatus.AUCTION_ORDERING,
    InstrumentStatus.AUCTION_BALANCE,
    InstrumentStatus.AUCTION_MATCH
]


class StatusEnterReason(Enum):
    """
    品种进入交易状态原因类型 hxxjava debug
    """
    AUTOMATIC = "自动切换"
    MANUAL = "手动切换"
    FUSE = "熔断" 


class MarginPriceType(Enum):
    """
    保证金价格类型  
    """
    # 昨结算价
    PRE_SETTLEMENT_PRICE = '1'
    # 最新价
    SETTLEMENT_PRICE = '2'
    # 成交均价
    $ERAGE_PRICE = '3'
    # 开仓价
    OPEN_PRICE = '4'


class AlgorithmType(Enum):
    """
    盈亏算法类型
    """
    # 浮盈浮亏都计算
    ALL = '1'
    # 浮盈不计,浮亏计
    ONLY_LOST ='2'
    # 浮盈计,浮亏不计
    ONLY_GAIN = '3'
    # 浮盈浮亏都不计算
    NONE = '4'


class IncludeCloseProfitType(Enum):
    """
    是否包含平仓盈利类型
    """
    # 包含平仓盈利
    INCLUDE = '0'
    # 不包含平仓盈利
    NOT_INCLUDE = '2'


class OptionRoyaltyPriceType(Enum):
    """
    期权权利金价格类型类型
    """
    # 昨结算价
    PRE_SETTLEMENT_PRICE = '1'
    # 开仓价
    OPEN_PRICE = '4'
    # 最新价与昨结算价较大值
    MAX_PRE_SETTLEMENT_PRICE = '5'


class IdCardType(Enum):
    """ $类型 """
    # 组织机构代码
    EID = '0'
    # 中国公民$
    IDCard = '1'
    # $
    OfficerIDCard = '2'
    # $
    PoliceIDCard = '3'
    # $
    SoldierIDCard = '4'
    # 户口簿
    HouseholdRegister = '5'
    # $
    Passport = '6'
    # 台胞证
    TaiwanCompatriotIDCard  = '7'
    # 回乡证
    HomeComingCard = '8'
    # 营业执照号
    LicenseNo = '9'
    # 税务登记号/当地纳税ID
    TaxNo = 'A'
    # 港澳居民来往内地通行证
    HMMainlandTravelPermit = 'B'
    # 台湾居民来往大陆通行证
    TwMainlandTravelPermit = 'C'
    # $
    DrivingLicense = 'D'
    # 当地社保ID
    SocialID = 'F'
    # 当地$
    LocalID = 'G'
    # 商业登记证
    BusinessRegistration = 'H'
    # 港澳永久性居民$
    HKMCIDCard = 'I'
    # 人行开户许可证
    AccountsPermits = 'J'
    # 外国人永久居留证
    FrgPrmtRdCard = 'K'
    # 资管产品备案函
    CptMngPrdLetter = 'L'
    # 统一社会信用代码
    UniformSocialCreditCode = 'N'
    # 机构成立证明文件
    CorporationCertNo = 'O'
    # 其他$
    OtherCard = 'x'


class ProductClass(Enum):
    # 期货
    FUTURES = '1'
    # 期货期权
    OPTIONS = '2'
    # 组合
    COMBINATION = '3'
    # 即期
    SPOT = '4'
    # 期转现
    EFP = '5'
    # 现货期权
    SPOT_OPTION = '6'
    # TAS合约
    TAS = '7'
    # 金属指数
    MI = 'I'

因为本文的内容比较长,一共分为5个帖子,其他内容在下方帖子中 ... ...

一个交易所可能有多个柜台连接,比如CTP柜台、恒牛柜台、金仕达柜台,不管来自哪个柜台,是不是报单编号和成交编号在一个交易 日内,在交易所内是唯一的吗?
报单编号是唯一的, 但是成交编号是不唯一的,因为撮合的双方,成交编号是一致的。也就是说一个报单可能在撮合过程中被拆分为多个成交单,交易所赋予成交单的相同的成交编号会被推送给多空双方,同时还包含了个不相同的报单编号。
另外,因为自成交情况存在,所以在写程序时需要注意,用成交方向+成交编号才能确定笔成交记录。

1. 消息定义

修改vnpy\trader\event.py,添加如下内容:

EVENT_ORIGIN_TICK = "eOriginTick."              # 原始tick消息
EVENT_AUCTION_TICK = "eAuctionTick."         # 集合竞价tick消息
EVENT_STATUS = "eStatus."                              # 交易状态消息
EVENT_STATUS_END = "eStatusEnd."              # 交易状态结束消息

2. 常量定义

修改vnpy\trader\constant.py,添加如下内容:

class InstrumentStatus(Enum):
    """
    合约交易状态类型 hxxjava debug
    """
    BEFORE_TRADING = "开盘前"
    NO_TRADING = "非交易"
    CONTINOUS = "连续交易" 
    AUCTION_ORDERING = "集合竞价报单"
    AUCTION_BALANCE = "集合竞价价格平衡"
    AUCTION_MATCH = "集合竞价撮合"
    CLOSE = "收盘"

# 有效交易状态
VALID_TRADE_STATUSES = [
    InstrumentStatus.CONTINOUS,
    InstrumentStatus.AUCTION_ORDERING,
    InstrumentStatus.AUCTION_BALANCE,
    InstrumentStatus.AUCTION_MATCH
]

# 集合竞价交易状态
AUCTION_STATUS = [
    InstrumentStatus.AUCTION_ORDERING,
    InstrumentStatus.AUCTION_BALANCE,
    InstrumentStatus.AUCTION_MATCH
]

class StatusEnterReason(Enum):
    """
    品种进入交易状态原因类型 hxxjava debug
    """
    AUTOMATIC = "自动切换"
    MANUAL = "手动切换"
    FUSE = "熔断"

3. 添加合约交易状态数据类型

修改vnpy\trader\object.py,添加如下内容:
3.1 在文件的前面添加这样的内容:

from .constant import InstrumentStatus,StatusEnterReason

3.2 在文件的后面添加下面的内容:

def left_alphas(instr:str):
    """ get lefe alphas of a string """
    ret_str = ''
    for s in instr:
        if s.isalpha():
            ret_str += s
        else:
            break
    return ret_str

@dataclass
class StatusData(BaseData):
    """
    hxxjava debug
    """
    symbol:str       
    exchange : Exchange    
    settlement_group_id : str = ""  
    instrument_status : InstrumentStatus = None   
    trading_segment_sn : int = None 
    enter_time : str = ""      
    enter_reason : StatusEnterReason = StatusEnterReason.AUTOMATIC 
    exchange_inst_id : str = ""     

    def __post_init__(self):
        """  """
        self.vt_symbol = f"{self.symbol}.{self.exchange.value}"

    def belongs_to(self,vt_symbol:str):
        symbol,exchange_str = vt_symbol.split(".")
        instrument = left_alphas(symbol).upper()
        return (self.symbol.upper() == instrument) and (self.exchange.value == exchange_str)

4. 网关修改

4.1 gateway修改

修改vnpy\trader\gateway.py,添加下面内容:
添加引用部分

from .event import EVENT_ORIGIN_TICK,EVENT_STATUS,  EVENT_STATUS_END
from .object import StatusData,     # hxxjava debug

这样修改on_tick():

    def on_tick(self, tick: TickData) -> None:
        """
        Tick event push.
        Tick event of a specific vt_symbol is also pushed.
        """     
        # self.on_event(EVENT_TICK, tick)
        # self.on_event(EVENT_TICK + tick.vt_symbol, tick)
        self.on_event(EVENT_ORIGIN_TICK, tick)

添加下面的两个函数:

    def on_status(self, status: StatusData) -> None:    # hxxjava debug
        """
        Instrument Status event push.
        """
        self.on_event(EVENT_STATUS, status)
        self.on_event(EVENT_STATUS + status.vt_symbol, status)

    def on_status_end(self, stats: List[StatusData]) -> None:    # hxxjava debug
        """
        Instrument Status list event push.
        """
        self.on_event(EVENT_STATUS_END, stats)

4.2 ctp_gateway修改

修改vnpy_ctp\gateway\ctp_gateway.py,步骤如下:

4.2.1 修改引用部分

from vnpy.trader.constant import InstrumentStatus,StatusEnterReason
from vnpy.trader.object import StatusData,     # hxxjava debug

4.2.2 修改CtpTdApi的构造函数init()

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

        self.reqid: int = 0
        self.order_ref: int = 0

        self.connect_status: bool = False
        self.login_status: bool = False
        self.auth_status: bool = False
        self.login_failed: bool = False
        self.contract_inited: bool = False

        self.userid: str = ""
        self.password: str = ""
        self.brokerid: str = ""
        self.auth_code: str = ""
        self.appid: str = ""

        self.frontid: int = 0
        self.sessionid: int = 0

        self.inited = False                    # hxxjava add
        self.status_data: List[dict] = []  # hxxjava add

        self.order_data: List[dict] = []
        self.trade_data: List[dict] = []
        self.positions: Dict[str, PositionData] = {}
        self.sysid_orderid_map: Dict[str, str] = {}

添加下面的两个函数:

    def onRtnInstrumentStatus(self,data:dict):
        """ 
        当接收到合约品种状态信息 # hxxjava debug 
        """
        if not self.contract_inited:
            self.status_data.append(data)
            return

        status = self.extractInstrumentStatus(data) 
        self.gateway.on_status(status)

    def extractInstrumentStatus(self,data:dict): # hxxjava add
        """ 提取合约品种状态信息 """
        return StatusData(
            symbol = data["InstrumentID"],
            exchange = EXCHANGE_CTP2VT[data["ExchangeID"]],
            settlement_group_id = data["SettlementGroupID"],
            instrument_status = INSTRUMENTSTATUS_CTP2VT[data["InstrumentStatus"]],
            trading_segment_sn = data["TradingSegmentSN"],
            enter_time = data["EnterTime"],
            enter_reason = ENTERREASON_CTP2VT[data["EnterReason"]],
            exchange_inst_id = data["ExchangeInstID"],
            gateway_name=self.gateway_name
        )

4.2.3 这样修改合约查询回报函数

    def onRspQryInstrument(self, data: dict, error: dict, reqid: int, last: bool) -> None:
        """合约查询回报"""
        product: Product = PRODUCT_CTP2VT.get(data["ProductClass"], None)
        if product:
            contract: ContractData = ContractData(
                symbol=data["InstrumentID"],
                exchange=EXCHANGE_CTP2VT[data["ExchangeID"]],
                name=data["InstrumentName"],
                product=product,
                size=data["VolumeMultiple"],
                pricetick=data["PriceTick"],
                # hxxjava add start
                max_market_order_volume=data["MaxMarketOrderVolume"],   
                min_market_order_volume=data["MinMarketOrderVolume"],
                max_limit_order_volume=data["MaxLimitOrderVolume"],
                min_limit_order_volume=data["MinLimitOrderVolume"],
                open_date=data["OpenDate"], 
                expire_date=data["ExpireDate"],
                is_trading=data["IsTrading"],
                long_margin_ratio=data["LongMarginRatio"],
                short_margin_ratio=data["ShortMarginRatio"],
                # hxxjava add end
                gateway_name=self.gateway_name
            )

            # 期权相关
            if contract.product == Product.OPTION:
                # 移除郑商所期权产品名称带有的C/P后缀
                if contract.exchange == Exchange.CZCE:
                    contract.option_portfolio = data["ProductID"][:-1]
                else:
                    contract.option_portfolio = data["ProductID"]

                contract.option_underlying = data["UnderlyingInstrID"]
                contract.option_type = OPTIONTYPE_CTP2VT.get(data["OptionsType"], None)
                contract.option_strike = data["StrikePrice"]
                contract.option_index = str(data["StrikePrice"])
                contract.option_expiry = datetime.strptime(data["ExpireDate"], "%Y%m%d")

            self.gateway.on_contract(contract)

            symbol_contract_map[contract.symbol] = contract

        if last:
            self.contract_inited = True
            self.gateway.write_log("合约信息查询成功")
            # self.gateway.write_log(f"收到{len(symbol_contract_map)}条合约信息")

            self.gateway.write_log(f"提取{len(self.status_data)}条状态信息")
            if self.status_data:
                statuses = []
                for data in self.status_data:
                    statuses.append(self.extractInstrumentStatus(data))
                self.gateway.on_status_end(statuses)
                self.status_data.clear()

            # self.gateway.write_log(f"提取{len(self.order_data)}条委托单信息")
            for data in self.order_data:
                self.onRtnOrder(data)
            self.order_data.clear()

            # self.gateway.write_log(f"提取{len(self.trade_data)}条成交单信息")
            for data in self.trade_data:
                self.onRtnTrade(data)
            self.trade_data.clear()

            self.inited = True

5. OmsEngine修改

修改vnpy\trader\engine.py,OmsEngine的代码如下:

class OmsEngine(BaseEngine):
    """
    Provides order management system function for VN Trader.
    """

    def __init__(self, main_engine: MainEngine, event_engine: EventEngine):
        """"""
        super(OmsEngine, self).__init__(main_engine, event_engine, "oms")

        self.ticks: Dict[str, TickData] = {}
        self.orders: Dict[str, OrderData] = {}
        self.trades: Dict[str, TradeData] = {}
        self.positions: Dict[str, PositionData] = {}
        self.accounts: Dict[str, AccountData] = {}
        self.contracts: Dict[str, ContractData] = {}
        self.quotes: Dict[str, QuoteData] = {}

        self.active_orders: Dict[str, OrderData] = {}
        self.active_quotes: Dict[str, QuoteData] = {}

        self.auction_ticks: Dict[str, List[TickData]] = {}  # hxxjava 集合竞价tick字典,每个品种一个列表
        self.statuses:Dict[str,StatusData] = {}             # hxxjava add

        self.add_function()
        self.register_event()

    def add_function(self) -> None:
        """Add query function to main engine."""
        self.main_engine.get_tick = self.get_tick
        self.main_engine.get_order = self.get_order
        self.main_engine.get_trade = self.get_trade
        self.main_engine.get_position = self.get_position
        self.main_engine.get_account = self.get_account
        self.main_engine.get_contract = self.get_contract
        self.main_engine.get_quote = self.get_quote

        self.main_engine.get_all_ticks = self.get_all_ticks
        self.main_engine.get_all_orders = self.get_all_orders
        self.main_engine.get_all_trades = self.get_all_trades
        self.main_engine.get_all_positions = self.get_all_positions
        self.main_engine.get_all_accounts = self.get_all_accounts
        self.main_engine.get_all_contracts = self.get_all_contracts
        self.main_engine.get_all_quotes = self.get_all_quotes
        self.main_engine.get_all_active_orders = self.get_all_active_orders
        self.main_engine.get_all_active_qutoes = self.get_all_active_quotes
        self.main_engine.get_status = self.get_status                   # hxxjava add

    def register_event(self) -> None:
        """"""
        self.event_engine.register(EVENT_TICK, self.process_tick_event)
        self.event_engine.register(EVENT_ORDER, self.process_order_event)
        self.event_engine.register(EVENT_TRADE, self.process_trade_event)
        self.event_engine.register(EVENT_POSITION, self.process_position_event)
        self.event_engine.register(EVENT_ACCOUNT, self.process_account_event)
        self.event_engine.register(EVENT_CONTRACT, self.process_contract_event)
        self.event_engine.register(EVENT_QUOTE, self.process_quote_event)
        self.event_engine.register(EVENT_ORIGIN_TICK, self.process_origin_tick_event) # hxxjava add
        self.event_engine.register(EVENT_STATUS, self.process_status_event)  # hxxjava add
        self.event_engine.register(EVENT_STATUS_END, self.process_status_end)  # hxxjava add

    def process_origin_tick_event(self, event: Event) -> None:
        """"""
        tick: TickData = event.data

        # 得到tick合约的当前交易状态
        vt_symbol = get_vt_instrument(tick.vt_symbol)
        status = self.statuses.get(vt_symbol,None)
        if status:
            if status.instrument_status in AUCTION_STATUS:
                # 收到了集合竞价时段tick

                # 保存集合竞价tick到品种列表
                if vt_symbol not in self.auction_ticks:
                    self.auction_ticks[vt_symbol] = []
                self.auction_ticks[vt_symbol].append(tick)

                # 发出集合竞价tick消息
                self.event_engine.put(Event(EVENT_AUCTION_TICK,tick))

                print(f"集合竞价状态={status} 收到tick={tick}")

            elif status.instrument_status == InstrumentStatus.CONTINOUS:
                # 其他时段的tick
                self.event_engine.put(Event(EVENT_TICK,tick))
                self.event_engine.put(Event(EVENT_TICK + tick.vt_symbol,tick))

        else:
            # 按说应该不存在这种情况
            self.event_engine.put(Event(EVENT_TICK,tick))
            self.event_engine.put(Event(EVENT_TICK + tick.vt_symbol,tick))            

    def process_tick_event(self, event: Event) -> None:
        """"""
        tick: TickData = event.data
        self.ticks[tick.vt_symbol] = tick

    def process_order_event(self, event: Event) -> None:
        """"""
        order: OrderData = event.data
        self.orders[order.vt_orderid] = order

    def process_trade_event(self, event: Event) -> None:
        """"""
        trade: TradeData = event.data
        self.trades[trade.vt_tradeid] = trade
        print(f"process_trade_event:{trade}")

    def process_position_event(self, event: Event) -> None:
        """"""
        position: PositionData = event.data
        self.positions[position.vt_positionid] = position

    def process_account_event(self, event: Event) -> None:
        """"""
        account: AccountData = event.data
        self.accounts[account.vt_accountid] = account

    def process_contract_event(self, event: Event) -> None:
        """"""
        contract: ContractData = event.data
        self.contracts[contract.vt_symbol] = contract

    def process_status_event(self, event: Event) -> None:   # hxxjava add
        """ 交易状态通知消息处理 """
        status:StatusData = event.data
        # print(f"process_status_event {status}")
        vt_symbol = status.vt_symbol
        pre_status = self.statuses.get(vt_symbol,None)
        self.statuses[vt_symbol] = status
        if pre_status and pre_status.instrument_status in AUCTION_STATUS \
            and status.instrument_status == InstrumentStatus.CONTINOUS:
            # 当从状态为集合竞价状态进入连续竞价状态之时,
            # 把所有集合竞价的tick时间变换为连续竞价开始,
            # 然后重新把tick发送到系统的信息循环之中
            ticks = self.auction_ticks.get(vt_symbol,[])
            for t in ticks:
                tick:TickData = copy(t)
                hh,mm,ss = status.enter_time.split(':')
                tick0 = copy(tick)
                tick.datetime = tick.datetime.replace(hour=int(hh),minute=int(mm),second=int(ss),microsecond=0) 
                print(f"集合竞价{tick0} 集合竞价后{tick}")
                self.event_engine.put(Event(EVENT_TICK,tick))
            self.ticks.clear()

    def process_status_end(self, event: Event) -> None:   # hxxjava add
        """ 交易状态通知消息处理 """
        statuses:List[StatusData] = event.data
        for status in statuses:
            self.statuses[status.vt_symbol] = status
            print(f"status:{status}")

    def process_quote_event(self, event: Event) -> None:
        """"""
        quote: QuoteData = event.data
        self.quotes[quote.vt_quoteid] = quote

        # If quote is active, then update data in dict.
        if quote.is_active():
            self.active_quotes[quote.vt_quoteid] = quote
        # Otherwise, pop inactive quote from in dict
        elif quote.vt_quoteid in self.active_quotes:
            self.active_quotes.pop(quote.vt_quoteid)

    def get_tick(self, vt_symbol: str) -> Optional[TickData]:
        """
        Get latest market tick data by vt_symbol.
        """
        return self.ticks.get(vt_symbol, None)

    def get_order(self, vt_orderid: str) -> Optional[OrderData]:
        """
        Get latest order data by vt_orderid.
        """
        return self.orders.get(vt_orderid, None)

    def get_trade(self, vt_tradeid: str) -> Optional[TradeData]:
        """
        Get trade data by vt_tradeid.
        """
        return self.trades.get(vt_tradeid, None)

    def get_position(self, vt_positionid: str) -> Optional[PositionData]:
        """
        Get latest position data by vt_positionid.
        """
        return self.positions.get(vt_positionid, None)

    def get_account(self, vt_accountid: str) -> Optional[AccountData]:
        """
        Get latest account data by vt_accountid.
        """
        return self.accounts.get(vt_accountid, None)

    def get_contract(self, vt_symbol: str) -> Optional[ContractData]:
        """
        Get contract data by vt_symbol.
        """
        return self.contracts.get(vt_symbol, None)

    def get_quote(self, vt_quoteid: str) -> Optional[QuoteData]:
        """
        Get latest quote data by vt_orderid.
        """
        return self.quotes.get(vt_quoteid, None)

    def get_all_ticks(self) -> List[TickData]:
        """
        Get all tick data.
        """
        return list(self.ticks.values())

    def get_all_orders(self) -> List[OrderData]:
        """
        Get all order data.
        """
        return list(self.orders.values())

    def get_all_trades(self) -> List[TradeData]:
        """
        Get all trade data.
        """
        return list(self.trades.values())

    def get_all_positions(self) -> List[PositionData]:
        """
        Get all position data.
        """
        return list(self.positions.values())

    def get_all_accounts(self) -> List[AccountData]:
        """
        Get all account data.
        """
        return list(self.accounts.values())

    def get_all_contracts(self) -> List[ContractData]:
        """
        Get all contract data.
        """
        return list(self.contracts.values())

    def get_all_quotes(self) -> List[QuoteData]:
        """
        Get all quote data.
        """
        return list(self.quotes.values())

    def get_status(self,vt_symbol:str) -> Optional[StatusData]:     # hxxjava add
        """
        Get the vt_symbol's status data.
        """
        return self.statuses.get(vt_symbol,None)

    def get_all_statuses(self) -> List[StatusData]:     # hxxjava add
        """
        Get the vt_symbol's status data.
        """
        return self.statuses.values()

    def get_all_active_orders(self, vt_symbol: str = "") -> List[OrderData]:
        """
        Get all active orders by vt_symbol.

        If vt_symbol is empty, return all active orders.
        """
        if not vt_symbol:
            return list(self.active_orders.values())
        else:
            active_orders = [
                order
                for order in self.active_orders.values()
                if order.vt_symbol == vt_symbol
            ]
            return active_orders

    def get_all_active_quotes(self, vt_symbol: str = "") -> List[QuoteData]:
        """
        Get all active quotes by vt_symbol.

        If vt_symbol is empty, return all active qutoes.
        """
        if not vt_symbol:
            return list(self.active_quotes.values())
        else:
            active_quotes = [
                quote
                for quote in self.active_quotes.values()
                if quote.vt_symbol == vt_symbol
            ]
            return active_quotes

6. app中如何获得集合竞价tick

6.1 主界面中如何显示集合竞价tick ?

修改vnpy\trader\ui\widget.py,内容如下:

"""
Basic widgets for VN Trader.
"""

import csv
import platform
from enum import Enum
from typing import Any, Dict
from copy import copy
from tzlocal import get_localzone

from PyQt5 import QtCore, QtGui, QtWidgets, Qt
import importlib_metadata

import vnpy
from vnpy.event import Event, EventEngine
from ..constant import Direction, Exchange, Offset, OrderType
from ..engine import MainEngine
from ..event import (
    EVENT_QUOTE,
    EVENT_AUCTION_TICK,
    EVENT_TICK,
    EVENT_TRADE,
    EVENT_ORDER,
    EVENT_POSITION,
    EVENT_ACCOUNT,
    EVENT_STRATEGY_ACCOUNT,     # hxxjava
    EVENT_LOG
)
from ..object import OrderRequest, SubscribeRequest, PositionData
from ..utility import load_json, save_json, get_digits
from ..setting import SETTING_FILENAME, SETTINGS


COLOR_LONG = QtGui.QColor("red")
COLOR_SHORT = QtGui.QColor("green")
COLOR_BID = QtGui.QColor(255, 174, 201)
COLOR_ASK = QtGui.QColor(160, 255, 160)
COLOR_BLACK = QtGui.QColor("black")


class BaseCell(QtWidgets.QTableWidgetItem):
    """
    General cell used in tablewidgets.
    """

    def __init__(self, content: Any, data: Any):
        """"""
        super(BaseCell, self).__init__()
        self.setTextAlignment(QtCore.Qt.AlignCenter)
        self.set_content(content, data)

    def set_content(self, content: Any, data: Any) -> None:
        """
        Set text content.
        """
        self.setText(str(content))
        self._data = data

    def get_data(self) -> Any:
        """
        Get data object.
        """
        return self._data


class EnumCell(BaseCell):
    """
    Cell used for showing enum data.
    """

    def __init__(self, content: str, data: Any):
        """"""
        super(EnumCell, self).__init__(content, data)

    def set_content(self, content: Any, data: Any) -> None:
        """
        Set text using enum.constant.value.
        """
        if content:
            super(EnumCell, self).set_content(content.value, data)


class DirectionCell(EnumCell):
    """
    Cell used for showing direction data.
    """

    def __init__(self, content: str, data: Any):
        """"""
        super(DirectionCell, self).__init__(content, data)

    def set_content(self, content: Any, data: Any) -> None:
        """
        Cell color is set according to direction.
        """
        super(DirectionCell, self).set_content(content, data)

        if content is Direction.SHORT:
            self.setForeground(COLOR_SHORT)
        else:
            self.setForeground(COLOR_LONG)


class BidCell(BaseCell):
    """
    Cell used for showing bid price and volume.
    """

    def __init__(self, content: Any, data: Any):
        """"""
        super(BidCell, self).__init__(content, data)

        self.setForeground(COLOR_BID)


class AskCell(BaseCell):
    """
    Cell used for showing ask price and volume.
    """

    def __init__(self, content: Any, data: Any):
        """"""
        super(AskCell, self).__init__(content, data)

        self.setForeground(COLOR_ASK)


class PnlCell(BaseCell):
    """
    Cell used for showing pnl data.
    """

    def __init__(self, content: Any, data: Any):
        """"""
        super(PnlCell, self).__init__(content, data)

    def set_content(self, content: Any, data: Any) -> None:
        """
        Cell color is set based on whether pnl is
        positive or negative.
        """
        super(PnlCell, self).set_content(content, data)

        if str(content).startswith("-"):
            self.setForeground(COLOR_SHORT)
        else:
            self.setForeground(COLOR_LONG)

class DateTimeCell(BaseCell):
    """
    Cell used for showing time string from datetime object.
    """

    local_tz = get_localzone()

    def __init__(self, content: Any, data: Any):
        """"""
        super(DateTimeCell, self).__init__(content, data)

    def set_content(self, content: Any, data: Any) -> None:
        """"""
        if content is None:
            return

        content = content.astimezone(self.local_tz)
        timestamp = content.strftime("%Y-%m-%d %H:%M:%S")

        millisecond = int(content.microsecond / 1000)
        if millisecond:
            timestamp = f"{timestamp}.{millisecond}"

        self.setText(timestamp)
        self._data = data

class TimeCell(BaseCell):
    """
    Cell used for showing time string from datetime object.
    """

    local_tz = get_localzone()

    def __init__(self, content: Any, data: Any):
        """"""
        super(TimeCell, self).__init__(content, data)

    def set_content(self, content: Any, data: Any) -> None:
        """"""
        if content is None:
            return

        content = content.astimezone(self.local_tz)
        timestamp = content.strftime("%H:%M:%S")

        millisecond = int(content.microsecond / 1000)
        if millisecond:
            timestamp = f"{timestamp}.{millisecond}"
        else:
            timestamp = f"{timestamp}.000"

        self.setText(timestamp)
        self._data = data


class MsgCell(BaseCell):
    """
    Cell used for showing msg data.
    """

    def __init__(self, content: str, data: Any):
        """"""
        super(MsgCell, self).__init__(content, data)
        self.setTextAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)


class BaseMonitor(QtWidgets.QTableWidget):
    """
    Monitor data update in VN Trader.
    """

    event_type: str = ""
    data_key: str = ""
    sorting: bool = False
    headers: Dict[str, dict] = {}

    signal: QtCore.pyqtSignal = QtCore.pyqtSignal(Event)

    def __init__(self, main_engine: MainEngine, event_engine: EventEngine):
        """"""
        super(BaseMonitor, self).__init__()

        self.main_engine: MainEngine = main_engine
        self.event_engine: EventEngine = event_engine
        self.cells: Dict[str, dict] = {}

        self.init_ui()
        self.load_setting()
        self.register_event()

    def __del__(self) -> None:
        """"""
        self.save_setting()

    def init_ui(self) -> None:
        """"""
        self.init_table()
        self.init_menu()

    def init_table(self) -> None:
        """
        Initialize table.
        """
        self.setColumnCount(len(self.headers))

        labels = [d["display"] for d in self.headers.values()]
        self.setHorizontalHeaderLabels(labels)

        self.verticalHeader().setVisible(False)
        self.setEditTriggers(self.NoEditTriggers)
        self.setAlternatingRowColors(True)
        self.setSortingEnabled(self.sorting)

    def init_menu(self) -> None:
        """
        Create right click menu.
        """
        self.menu = QtWidgets.QMenu(self)

        resize_action = QtWidgets.QAction("调整列宽", self)
        resize_action.triggered.connect(self.resize_columns)
        self.menu.addAction(resize_action)

        save_action = QtWidgets.QAction("保存数据", self)
        save_action.triggered.connect(self.save_csv)
        self.menu.addAction(save_action)

    def register_event(self) -> None:
        """
        Register event handler into event engine.
        """
        if self.event_type:
            self.signal.connect(self.process_event)
            if type(self.event_type) == list: # hxxjava debug
                for ev in self.event_type:
                    self.event_engine.register(ev, self.signal.emit)
                    # print(f"已经订阅 {ev} 消息")
            else:
                self.event_engine.register(self.event_type, self.signal.emit)


    def process_event(self, event: Event) -> None:
        """
        Process new data from event and update into table.
        """
        # Disable sorting to prevent unwanted error.
        if self.sorting:
            self.setSortingEnabled(False)

        # Update data into table.
        data = event.data

        if not self.data_key:
            self.insert_new_row(data)
        else:
            key = data.__getattribute__(self.data_key)

            if key in self.cells:
                self.update_old_row(data)
            else:
                self.insert_new_row(data)

        # Enable sorting
        if self.sorting:
            self.setSortingEnabled(True)

    def insert_new_row(self, data: Any):
        """
        Insert a new row at the top of table.
        """
        self.insertRow(0)

        row_cells = {}
        for column, header in enumerate(self.headers.keys()):
            setting = self.headers[header]

            content = data.__getattribute__(header)
            cell = setting["cell"](content, data)
            self.setItem(0, column, cell)

            if setting["update"]:
                row_cells[header] = cell

        if self.data_key:
            key = data.__getattribute__(self.data_key)
            self.cells[key] = row_cells

    def update_old_row(self, data: Any) -> None:
        """
        Update an old row in table.
        """
        key = data.__getattribute__(self.data_key)
        row_cells = self.cells[key]

        for header, cell in row_cells.items():
            content = data.__getattribute__(header)
            cell.set_content(content, data)

    def resize_columns(self) -> None:
        """
        Resize all columns according to contents.
        """
        self.horizontalHeader().resizeSections(QtWidgets.QHeaderView.ResizeToContents)

    def save_csv(self) -> None:
        """
        Save table data into a csv file
        """
        path, _ = QtWidgets.QFileDialog.getSaveFileName(
            self, "保存数据", "", "CSV(*.csv)")

        if not path:
            return

        with open(path, "w") as f:
            writer = csv.writer(f, lineterminator="\n")

            headers = [d["display"] for d in self.headers.values()]
            writer.writerow(headers)

            for row in range(self.rowCount()):
                if self.isRowHidden(row):
                    continue

                row_data = []
                for column in range(self.columnCount()):
                    item = self.item(row, column)
                    if item:
                        row_data.append(str(item.text()))
                    else:
                        row_data.append("")
                writer.writerow(row_data)

    def contextMenuEvent(self, event: QtGui.QContextMenuEvent) -> None:
        """
        Show menu with right click.
        """
        self.menu.popup(QtGui.QCursor.pos())

    def save_setting(self) -> None:
        """"""
        settings = QtCore.QSettings(self.__class__.__name__, "custom")
        settings.setValue("column_state", self.horizontalHeader().saveState())

    def load_setting(self) -> None:
        """"""
        settings = QtCore.QSettings(self.__class__.__name__, "custom")
        column_state = settings.value("column_state")

        if isinstance(column_state, QtCore.QByteArray):
            self.horizontalHeader().restoreState(column_state)
            self.horizontalHeader().setSortIndicator(-1, QtCore.Qt.AscendingOrder)


class TickMonitor(BaseMonitor):
    """
    Monitor for tick data.
    """
    # event_type = EVENT_TICK
    event_type = [EVENT_TICK,EVENT_AUCTION_TICK]
    data_key = "vt_symbol"
    sorting = True

    headers = {
        "symbol": {"display": "代码", "cell": BaseCell, "update": False},
        "exchange": {"display": "交易所", "cell": EnumCell, "update": False},
        "name": {"display": "名称", "cell": BaseCell, "update": True},
        "last_price": {"display": "最新价", "cell": BaseCell, "update": True},
        "volume": {"display": "成交量", "cell": BaseCell, "update": True},
        "open_price": {"display": "开盘价", "cell": BaseCell, "update": True},
        "high_price": {"display": "最高价", "cell": BaseCell, "update": True},
        "low_price": {"display": "最低价", "cell": BaseCell, "update": True},
        "bid_price_1": {"display": "买1价", "cell": BidCell, "update": True},
        "bid_volume_1": {"display": "买1量", "cell": BidCell, "update": True},
        "ask_price_1": {"display": "卖1价", "cell": AskCell, "update": True},
        "ask_volume_1": {"display": "卖1量", "cell": AskCell, "update": True},
        "datetime": {"display": "时间", "cell": TimeCell, "update": True},
        "gateway_name": {"display": "接口", "cell": BaseCell, "update": False},
    }


class LogMonitor(BaseMonitor):
    """
    Monitor for log data.
    """

    event_type = EVENT_LOG
    data_key = ""
    sorting = False

    headers = {
        "time": {"display": "时间", "cell": TimeCell, "update": False},
        "msg": {"display": "信息", "cell": MsgCell, "update": False},
        "gateway_name": {"display": "接口", "cell": BaseCell, "update": False},
    }


class TradeMonitor(BaseMonitor):
    """
    Monitor for trade data.
    """

    event_type = EVENT_TRADE
    data_key = "tradeid"    # hxxjava chanage
    sorting = True

    headers: Dict[str, dict] = {
        "tradeid": {"display": "成交号 ", "cell": BaseCell, "update": False},
        "orderid": {"display": "委托号", "cell": BaseCell, "update": False},
        "symbol": {"display": "代码", "cell": BaseCell, "update": False},
        "exchange": {"display": "交易所", "cell": EnumCell, "update": False},
        "direction": {"display": "方向", "cell": DirectionCell, "update": False},
        "offset": {"display": "开平", "cell": EnumCell, "update": False},
        "price": {"display": "价格", "cell": BaseCell, "update": False},
        "volume": {"display": "数量", "cell": BaseCell, "update": False},
        "datetime": {"display": "时间", "cell": DateTimeCell, "update": False},
        "gateway_name": {"display": "接口", "cell": BaseCell, "update": False},
        # "reference": {"display": "策略", "cell": BaseCell, "update": False},
    }


class OrderMonitor(BaseMonitor):
    """
    Monitor for order data.
    """

    event_type = EVENT_ORDER
    data_key = "vt_orderid"
    sorting = True

    headers: Dict[str, dict] = {
        "orderid": {"display": "委托号", "cell": BaseCell, "update": False},
        "reference": {"display": "来源", "cell": BaseCell, "update": False},
        "symbol": {"display": "代码", "cell": BaseCell, "update": False},
        "exchange": {"display": "交易所", "cell": EnumCell, "update": False},
        "type": {"display": "类型", "cell": EnumCell, "update": False},
        "direction": {"display": "方向", "cell": DirectionCell, "update": False},
        "offset": {"display": "开平", "cell": EnumCell, "update": False},
        "price": {"display": "价格", "cell": BaseCell, "update": False},
        "volume": {"display": "总数量", "cell": BaseCell, "update": True},
        "traded": {"display": "已成交", "cell": BaseCell, "update": True},
        "status": {"display": "状态", "cell": EnumCell, "update": True},
        "datetime": {"display": "时间", "cell": DateTimeCell, "update": True},
        "gateway_name": {"display": "接口", "cell": BaseCell, "update": False},
    }

    def init_ui(self):
        """
        Connect signal.
        """
        super(OrderMonitor, self).init_ui()

        self.setToolTip("双击单元格撤单")
        self.itemDoubleClicked.connect(self.cancel_order)

    def cancel_order(self, cell: BaseCell) -> None:
        """
        Cancel order if cell double clicked.
        """
        order = cell.get_data()
        req = order.create_cancel_request()
        self.main_engine.cancel_order(req, order.gateway_name)


class PositionMonitor(BaseMonitor):
    """
    Monitor for position data.
    """

    event_type = EVENT_POSITION
    data_key = "vt_positionid"
    sorting = True

    headers = {
        "symbol": {"display": "代码", "cell": BaseCell, "update": False},
        "exchange": {"display": "交易所", "cell": EnumCell, "update": False},
        "direction": {"display": "方向", "cell": DirectionCell, "update": False},
        "volume": {"display": "数量", "cell": BaseCell, "update": True},
        "yd_volume": {"display": "昨仓", "cell": BaseCell, "update": True},
        "frozen": {"display": "冻结", "cell": BaseCell, "update": True},
        "price": {"display": "均价", "cell": BaseCell, "update": True},
        "pnl": {"display": "盈亏", "cell": PnlCell, "update": True},
        "gateway_name": {"display": "接口", "cell": BaseCell, "update": False},
    }


class AccountMonitor(BaseMonitor):
    """
    Monitor for account data.
    """

    event_type = EVENT_ACCOUNT
    data_key = "vt_accountid"
    sorting = True

    headers = {
        "accountid": {"display": "账号", "cell": BaseCell, "update": False},
        "balance": {"display": "余额", "cell": BaseCell, "update": True},
        "frozen": {"display": "冻结", "cell": BaseCell, "update": True},
        "available": {"display": "可用", "cell": BaseCell, "update": True},
        "gateway_name": {"display": "接口", "cell": BaseCell, "update": False},
    }


class QuoteMonitor(BaseMonitor):
    """
    Monitor for quote data.
    """

    event_type = EVENT_QUOTE
    data_key = "vt_quoteid"
    sorting = True

    headers: Dict[str, dict] = {
        "quoteid": {"display": "报价号", "cell": BaseCell, "update": False},
        "reference": {"display": "来源", "cell": BaseCell, "update": False},
        "symbol": {"display": "代码", "cell": BaseCell, "update": False},
        "exchange": {"display": "交易所", "cell": EnumCell, "update": False},
        "bid_offset": {"display": "买开平", "cell": EnumCell, "update": False},
        "bid_volume": {"display": "买量", "cell": BidCell, "update": False},
        "bid_price": {"display": "买价", "cell": BidCell, "update": False},
        "ask_price": {"display": "卖价", "cell": AskCell, "update": False},
        "ask_volume": {"display": "卖量", "cell": AskCell, "update": False},
        "ask_offset": {"display": "卖开平", "cell": EnumCell, "update": False},
        "status": {"display": "状态", "cell": EnumCell, "update": True},
        "datetime": {"display": "时间", "cell": TimeCell, "update": True},
        "gateway_name": {"display": "接口", "cell": BaseCell, "update": False},
    }

    def init_ui(self):
        """
        Connect signal.
        """
        super().init_ui()

        self.setToolTip("双击单元格撤销报价")
        self.itemDoubleClicked.connect(self.cancel_quote)

    def cancel_quote(self, cell: BaseCell) -> None:
        """
        Cancel quote if cell double clicked.
        """
        quote = cell.get_data()
        req = quote.create_cancel_request()
        self.main_engine.cancel_quote(req, quote.gateway_name)

class StrategyAccountMonitor(BaseMonitor):      # hxxjava add
    """
    Monitor for account data.
    """

    event_type = EVENT_STRATEGY_ACCOUNT
    data_key = "strategy_name"
    sorting = True

    headers = {
        "strategy_name": {"display": "策略", "cell": BaseCell, "update": False},
        "capital": {"display": "本金", "cell": BaseCell, "update": True},
        "money": {"display": "权益", "cell": BaseCell, "update": True},
        "margin": {"display": "保证金", "cell": BaseCell, "update": True},
        "available": {"display": "可用资金", "cell": BaseCell, "update": True},
        "commission": {"display": "手续费", "cell": BaseCell, "update": True},
    }


class ConnectDialog(QtWidgets.QDialog):
    """
    Start connection of a certain gateway.
    """

    def __init__(self, main_engine: MainEngine, gateway_name: str):
        """"""
        super().__init__()

        self.main_engine: MainEngine = main_engine
        self.gateway_name: str = gateway_name
        self.filename: str = f"connect_{gateway_name.lower()}.json"

        self.widgets: Dict[str, QtWidgets.QWidget] = {}

        self.init_ui()

    def init_ui(self) -> None:
        """"""
        self.setWindowTitle(f"连接{self.gateway_name}")

        # Default setting provides field name, field data type and field default value.
        default_setting = self.main_engine.get_default_setting(
            self.gateway_name)

        # Saved setting provides field data used last time.
        loaded_setting = load_json(self.filename)

        # Initialize line edits and form layout based on setting.
        form = QtWidgets.QFormLayout()

        for field_name, field_value in default_setting.items():
            field_type = type(field_value)

            if field_type == list:
                widget = QtWidgets.QComboBox()
                widget.addItems(field_value)

                if field_name in loaded_setting:
                    saved_value = loaded_setting[field_name]
                    ix = widget.findText(saved_value)
                    widget.setCurrentIndex(ix)
            else:
                widget = QtWidgets.QLineEdit(str(field_value))

                if field_name in loaded_setting:
                    saved_value = loaded_setting[field_name]
                    widget.setText(str(saved_value))

                if "密码" in field_name:
                    widget.setEchoMode(QtWidgets.QLineEdit.Password)

                if field_type == int:
                    validator = QtGui.QIntValidator()
                    widget.setValidator(validator)

            form.addRow(f"{field_name} <{field_type.__name__}>", widget)
            self.widgets[field_name] = (widget, field_type)

        button = QtWidgets.QPushButton("连接")
        button.clicked.connect(self.connect)
        form.addRow(button)

        self.setLayout(form)

    def connect(self) -> None:
        """
        Get setting value from line edits and connect the gateway.
        """
        setting = {}
        for field_name, tp in self.widgets.items():
            widget, field_type = tp
            if field_type == list:
                field_value = str(widget.currentText())
            else:
                try:
                    field_value = field_type(widget.text())
                except ValueError:
                    field_value = field_type()
            setting[field_name] = field_value

        save_json(self.filename, setting)

        self.main_engine.connect(setting, self.gateway_name)
        self.accept()


class TradingWidget(QtWidgets.QWidget):
    """
    General manual trading widget.
    """

    signal_tick = QtCore.pyqtSignal(Event)

    def __init__(self, main_engine: MainEngine, event_engine: EventEngine):
        """"""
        super().__init__()

        self.main_engine: MainEngine = main_engine
        self.event_engine: EventEngine = event_engine

        self.vt_symbol: str = ""
        self.price_digits: int = 0

        self.init_ui()
        self.register_event()

    def init_ui(self) -> None:
        """"""
        self.setFixedWidth(300)

        # Trading function area
        exchanges = self.main_engine.get_all_exchanges()
        self.exchange_combo = QtWidgets.QComboBox()
        self.exchange_combo.addItems([exchange.value for exchange in exchanges])

        self.symbol_line = QtWidgets.QLineEdit()
        self.symbol_line.returnPressed.connect(self.set_vt_symbol)

        self.name_line = QtWidgets.QLineEdit()
        self.name_line.setReadOnly(True)

        self.direction_combo = QtWidgets.QComboBox()
        self.direction_combo.addItems(
            [Direction.LONG.value, Direction.SHORT.value])

        self.offset_combo = QtWidgets.QComboBox()
        self.offset_combo.addItems([offset.value for offset in Offset])

        self.order_type_combo = QtWidgets.QComboBox()
        self.order_type_combo.addItems(
            [order_type.value for order_type in OrderType])

        double_validator = QtGui.QDoubleValidator()
        double_validator.setBottom(0)

        self.price_line = QtWidgets.QLineEdit()
        self.price_line.setValidator(double_validator)

        self.volume_line = QtWidgets.QLineEdit()
        self.volume_line.setValidator(double_validator)

        self.gateway_combo = QtWidgets.QComboBox()
        self.gateway_combo.addItems(self.main_engine.get_all_gateway_names())

        self.price_check = QtWidgets.QCheckBox()
        self.price_check.setToolTip("设置价格随行情更新")

        send_button = QtWidgets.QPushButton("委托")
        send_button.clicked.connect(self.send_order)

        cancel_button = QtWidgets.QPushButton("全撤")
        cancel_button.clicked.connect(self.cancel_all)

        grid = QtWidgets.QGridLayout()
        grid.addWidget(QtWidgets.QLabel("交易所"), 0, 0)
        grid.addWidget(QtWidgets.QLabel("代码"), 1, 0)
        grid.addWidget(QtWidgets.QLabel("名称"), 2, 0)
        grid.addWidget(QtWidgets.QLabel("方向"), 3, 0)
        grid.addWidget(QtWidgets.QLabel("开平"), 4, 0)
        grid.addWidget(QtWidgets.QLabel("类型"), 5, 0)
        grid.addWidget(QtWidgets.QLabel("价格"), 6, 0)
        grid.addWidget(QtWidgets.QLabel("数量"), 7, 0)
        grid.addWidget(QtWidgets.QLabel("接口"), 8, 0)
        grid.addWidget(self.exchange_combo, 0, 1, 1, 2)
        grid.addWidget(self.symbol_line, 1, 1, 1, 2)
        grid.addWidget(self.name_line, 2, 1, 1, 2)
        grid.addWidget(self.direction_combo, 3, 1, 1, 2)
        grid.addWidget(self.offset_combo, 4, 1, 1, 2)
        grid.addWidget(self.order_type_combo, 5, 1, 1, 2)
        grid.addWidget(self.price_line, 6, 1, 1, 1)
        grid.addWidget(self.price_check, 6, 2, 1, 1)
        grid.addWidget(self.volume_line, 7, 1, 1, 2)
        grid.addWidget(self.gateway_combo, 8, 1, 1, 2)
        grid.addWidget(send_button, 9, 0, 1, 3)
        grid.addWidget(cancel_button, 10, 0, 1, 3)

        # Market depth display area
        bid_color = "rgb(255,174,201)"
        ask_color = "rgb(160,255,160)"

        self.bp1_label = self.create_label(bid_color)
        self.bp2_label = self.create_label(bid_color)
        self.bp3_label = self.create_label(bid_color)
        self.bp4_label = self.create_label(bid_color)
        self.bp5_label = self.create_label(bid_color)

        self.bv1_label = self.create_label(
            bid_color, alignment=QtCore.Qt.AlignRight)
        self.bv2_label = self.create_label(
            bid_color, alignment=QtCore.Qt.AlignRight)
        self.bv3_label = self.create_label(
            bid_color, alignment=QtCore.Qt.AlignRight)
        self.bv4_label = self.create_label(
            bid_color, alignment=QtCore.Qt.AlignRight)
        self.bv5_label = self.create_label(
            bid_color, alignment=QtCore.Qt.AlignRight)

        self.ap1_label = self.create_label(ask_color)
        self.ap2_label = self.create_label(ask_color)
        self.ap3_label = self.create_label(ask_color)
        self.ap4_label = self.create_label(ask_color)
        self.ap5_label = self.create_label(ask_color)

        self.av1_label = self.create_label(
            ask_color, alignment=QtCore.Qt.AlignRight)
        self.av2_label = self.create_label(
            ask_color, alignment=QtCore.Qt.AlignRight)
        self.av3_label = self.create_label(
            ask_color, alignment=QtCore.Qt.AlignRight)
        self.av4_label = self.create_label(
            ask_color, alignment=QtCore.Qt.AlignRight)
        self.av5_label = self.create_label(
            ask_color, alignment=QtCore.Qt.AlignRight)

        self.lp_label = self.create_label()
        self.return_label = self.create_label(alignment=QtCore.Qt.AlignRight)

        form = QtWidgets.QFormLayout()
        form.addRow(self.ap5_label, self.av5_label)
        form.addRow(self.ap4_label, self.av4_label)
        form.addRow(self.ap3_label, self.av3_label)
        form.addRow(self.ap2_label, self.av2_label)
        form.addRow(self.ap1_label, self.av1_label)
        form.addRow(self.lp_label, self.return_label)
        form.addRow(self.bp1_label, self.bv1_label)
        form.addRow(self.bp2_label, self.bv2_label)
        form.addRow(self.bp3_label, self.bv3_label)
        form.addRow(self.bp4_label, self.bv4_label)
        form.addRow(self.bp5_label, self.bv5_label)

        # Overall layout
        vbox = QtWidgets.QVBoxLayout()
        vbox.addLayout(grid)
        vbox.addLayout(form)
        self.setLayout(vbox)

    def create_label(
        self,
        color: str = "",
        alignment: int = QtCore.Qt.AlignLeft
    ) -> QtWidgets.QLabel:
        """
        Create label with certain font color.
        """
        label = QtWidgets.QLabel()
        if color:
            label.setStyleSheet(f"color:{color}")
        label.setAlignment(alignment)
        return label

    def register_event(self) -> None:
        """"""
        self.signal_tick.connect(self.process_tick_event)
        self.event_engine.register(EVENT_TICK, self.signal_tick.emit)

    def process_tick_event(self, event: Event) -> None:
        """"""
        tick = event.data
        if tick.vt_symbol != self.vt_symbol:
            return

        price_digits = self.price_digits

        self.lp_label.setText(f"{tick.last_price:.{price_digits}f}")
        self.bp1_label.setText(f"{tick.bid_price_1:.{price_digits}f}")
        self.bv1_label.setText(str(tick.bid_volume_1))
        self.ap1_label.setText(f"{tick.ask_price_1:.{price_digits}f}")
        self.av1_label.setText(str(tick.ask_volume_1))

        if tick.pre_close:
            r = (tick.last_price / tick.pre_close - 1) * 100
            self.return_label.setText(f"{r:.2f}%")

        if tick.bid_price_2:
            self.bp2_label.setText(f"{tick.bid_price_2:.{price_digits}f}")
            self.bv2_label.setText(str(tick.bid_volume_2))
            self.ap2_label.setText(f"{tick.ask_price_2:.{price_digits}f}")
            self.av2_label.setText(str(tick.ask_volume_2))

            self.bp3_label.setText(f"{tick.bid_price_3:.{price_digits}f}")
            self.bv3_label.setText(str(tick.bid_volume_3))
            self.ap3_label.setText(f"{tick.ask_price_3:.{price_digits}f}")
            self.av3_label.setText(str(tick.ask_volume_3))

            self.bp4_label.setText(f"{tick.bid_price_4:.{price_digits}f}")
            self.bv4_label.setText(str(tick.bid_volume_4))
            self.ap4_label.setText(f"{tick.ask_price_4:.{price_digits}f}")
            self.av4_label.setText(str(tick.ask_volume_4))

            self.bp5_label.setText(f"{tick.bid_price_5:.{price_digits}f}")
            self.bv5_label.setText(str(tick.bid_volume_5))
            self.ap5_label.setText(f"{tick.ask_price_5:.{price_digits}f}")
            self.av5_label.setText(str(tick.ask_volume_5))

        if self.price_check.isChecked():
            self.price_line.setText(f"{tick.last_price:.{price_digits}f}")

    def set_vt_symbol(self) -> None:
        """
        Set the tick depth data to monitor by vt_symbol.
        """
        symbol = str(self.symbol_line.text())
        if not symbol:
            return

        # Generate vt_symbol from symbol and exchange
        exchange_value = str(self.exchange_combo.currentText())
        vt_symbol = f"{symbol}.{exchange_value}"

        if vt_symbol == self.vt_symbol:
            return
        self.vt_symbol = vt_symbol

        # Update name line widget and clear all labels
        contract = self.main_engine.get_contract(vt_symbol)
        if not contract:
            self.name_line.setText("")
            gateway_name = self.gateway_combo.currentText()
        else:
            self.name_line.setText(contract.name)
            gateway_name = contract.gateway_name

            # Update gateway combo box.
            ix = self.gateway_combo.findText(gateway_name)
            self.gateway_combo.setCurrentIndex(ix)

            # Update price digits
            self.price_digits = get_digits(contract.pricetick)

        self.clear_label_text()
        self.volume_line.setText("")
        self.price_line.setText("")

        # Subscribe tick data
        req = SubscribeRequest(
            symbol=symbol, exchange=Exchange(exchange_value)
        )

        self.main_engine.subscribe(req, gateway_name)

    def clear_label_text(self) -> None:
        """
        Clear text on all labels.
        """
        self.lp_label.setText("")
        self.return_label.setText("")

        self.bv1_label.setText("")
        self.bv2_label.setText("")
        self.bv3_label.setText("")
        self.bv4_label.setText("")
        self.bv5_label.setText("")

        self.av1_label.setText("")
        self.av2_label.setText("")
        self.av3_label.setText("")
        self.av4_label.setText("")
        self.av5_label.setText("")

        self.bp1_label.setText("")
        self.bp2_label.setText("")
        self.bp3_label.setText("")
        self.bp4_label.setText("")
        self.bp5_label.setText("")

        self.ap1_label.setText("")
        self.ap2_label.setText("")
        self.ap3_label.setText("")
        self.ap4_label.setText("")
        self.ap5_label.setText("")

    def send_order(self) -> None:
        """
        Send new order manually.
        """
        symbol = str(self.symbol_line.text())
        if not symbol:
            QtWidgets.QMessageBox.critical(self, "委托失败", "请输入合约代码")
            return

        volume_text = str(self.volume_line.text())
        if not volume_text:
            QtWidgets.QMessageBox.critical(self, "委托失败", "请输入委托数量")
            return
        volume = float(volume_text)

        price_text = str(self.price_line.text())
        if not price_text:
            price = 0
        else:
            price = float(price_text)

        req = OrderRequest(
            symbol=symbol,
            exchange=Exchange(str(self.exchange_combo.currentText())),
            direction=Direction(str(self.direction_combo.currentText())),
            type=OrderType(str(self.order_type_combo.currentText())),
            volume=volume,
            price=price,
            offset=Offset(str(self.offset_combo.currentText())),
            reference="ManualTrading"
        )

        gateway_name = str(self.gateway_combo.currentText())

        self.main_engine.send_order(req, gateway_name)

    def cancel_all(self) -> None:
        """
        Cancel all active orders.
        """
        order_list = self.main_engine.get_all_active_orders()
        for order in order_list:
            req = order.create_cancel_request()
            self.main_engine.cancel_order(req, order.gateway_name)

    def update_with_cell(self, cell: BaseCell) -> None:
        """"""
        data = cell.get_data()

        self.symbol_line.setText(data.symbol)
        self.exchange_combo.setCurrentIndex(
            self.exchange_combo.findText(data.exchange.value)
        )

        self.set_vt_symbol()

        if isinstance(data, PositionData):
            if data.direction == Direction.SHORT:
                direction = Direction.LONG
            elif data.direction == Direction.LONG:
                direction = Direction.SHORT
            else:       # Net position mode
                if data.volume > 0:
                    direction = Direction.SHORT
                else:
                    direction = Direction.LONG

            self.direction_combo.setCurrentIndex(
                self.direction_combo.findText(direction.value)
            )
            self.offset_combo.setCurrentIndex(
                self.offset_combo.findText(Offset.CLOSE.value)
            )
            self.volume_line.setText(str(abs(data.volume)))


class ActiveOrderMonitor(OrderMonitor):
    """
    Monitor which shows active order only.
    """

    def process_event(self, event) -> None:
        """
        Hides the row if order is not active.
        """
        super(ActiveOrderMonitor, self).process_event(event)

        order = event.data
        row_cells = self.cells[order.vt_orderid]
        row = self.row(row_cells["volume"])

        if order.is_active():
            self.showRow(row)
        else:
            self.hideRow(row)


class ContractManager(QtWidgets.QWidget):
    """
    Query contract data available to trade in system.
    """

    headers: Dict[str, str] = {
        "vt_symbol": "本地代码",
        "symbol": "代码",
        "exchange": "交易所",
        "name": "名称",
        "product": "合约分类",
        "size": "合约乘数",
        "pricetick": "价格跳动",
        "min_volume": "最小委托量",
        "gateway_name": "交易接口",
    }

    def __init__(self, main_engine: MainEngine, event_engine: EventEngine):
        super().__init__()

        self.main_engine: MainEngine = main_engine
        self.event_engine: EventEngine = event_engine

        self.init_ui()

    def init_ui(self) -> None:
        """"""
        self.setWindowTitle("合约查询")
        self.resize(1000, 600)

        self.filter_line = QtWidgets.QLineEdit()
        self.filter_line.setPlaceholderText("输入合约代码或者交易所,留空则查询所有合约")

        self.button_show = QtWidgets.QPushButton("查询")
        self.button_show.clicked.connect(self.show_contracts)

        labels = []
        for name, display in self.headers.items():
            label = f"{display}\n{name}"
            labels.append(label)

        self.contract_table = QtWidgets.QTableWidget()
        self.contract_table.setColumnCount(len(self.headers))
        self.contract_table.setHorizontalHeaderLabels(labels)
        self.contract_table.verticalHeader().setVisible(False)
        self.contract_table.setEditTriggers(self.contract_table.NoEditTriggers)
        self.contract_table.setAlternatingRowColors(True)

        hbox = QtWidgets.QHBoxLayout()
        hbox.addWidget(self.filter_line)
        hbox.addWidget(self.button_show)

        vbox = QtWidgets.QVBoxLayout()
        vbox.addLayout(hbox)
        vbox.addWidget(self.contract_table)

        self.setLayout(vbox)

    def show_contracts(self) -> None:
        """
        Show contracts by symbol
        """
        flt = str(self.filter_line.text())

        all_contracts = self.main_engine.get_all_contracts()
        if flt:
            contracts = [
                contract for contract in all_contracts if flt in contract.vt_symbol
            ]
        else:
            contracts = all_contracts

        self.contract_table.clearContents()
        self.contract_table.setRowCount(len(contracts))

        for row, contract in enumerate(contracts):
            for column, name in enumerate(self.headers.keys()):
                value = getattr(contract, name)
                if isinstance(value, Enum):
                    cell = EnumCell(value, contract)
                else:
                    cell = BaseCell(value, contract)
                self.contract_table.setItem(row, column, cell)

        self.contract_table.resizeColumnsToContents()


class AboutDialog(QtWidgets.QDialog):
    """
    About VN Trader.
    """

    def __init__(self, main_engine: MainEngine, event_engine: EventEngine):
        """"""
        super().__init__()

        self.main_engine: MainEngine = main_engine
        self.event_engine: EventEngine = event_engine

        self.init_ui()

    def init_ui(self) -> None:
        """"""
        self.setWindowTitle("关于VN Trader")

        text = f"""
            By Traders, For Traders.


            License:MIT
            Website:www.vnpy.com
            Github:www.github.com/vnpy/vnpy


            vn.py - {vnpy.__version__}
            Python - {platform.python_version()}
            PyQt5 - {Qt.PYQT_VERSION_STR}
            NumPy - {importlib_metadata.version("numpy")}
            pandas - {importlib_metadata.version("pandas")}
            RQData - {importlib_metadata.version("rqdatac")}
            """

        label = QtWidgets.QLabel()
        label.setText(text)
        label.setMinimumWidth(500)

        vbox = QtWidgets.QVBoxLayout()
        vbox.addWidget(label)
        self.setLayout(vbox)


class GlobalDialog(QtWidgets.QDialog):
    """
    Start connection of a certain gateway.
    """

    def __init__(self):
        """"""
        super().__init__()

        self.widgets: Dict[str, Any] = {}

        self.init_ui()

    def init_ui(self) -> None:
        """"""
        self.setWindowTitle("全局配置")
        self.setMinimumWidth(800)

        settings = copy(SETTINGS)
        settings.update(load_json(SETTING_FILENAME))

        # Initialize line edits and form layout based on setting.
        form = QtWidgets.QFormLayout()

        for field_name, field_value in settings.items():
            field_type = type(field_value)
            widget = QtWidgets.QLineEdit(str(field_value))

            form.addRow(f"{field_name} <{field_type.__name__}>", widget)
            self.widgets[field_name] = (widget, field_type)

        button = QtWidgets.QPushButton("确定")
        button.clicked.connect(self.update_setting)
        form.addRow(button)

        scroll_widget = QtWidgets.QWidget()
        scroll_widget.setLayout(form)

        scroll_area = QtWidgets.QScrollArea()
        scroll_area.setWidgetResizable(True)
        scroll_area.setWidget(scroll_widget)

        vbox = QtWidgets.QVBoxLayout()
        vbox.addWidget(scroll_area)
        self.setLayout(vbox)

    def update_setting(self) -> None:
        """
        Get setting value from line edits and update global setting file.
        """
        settings = {}
        for field_name, tp in self.widgets.items():
            widget, field_type = tp
            value_text = widget.text()

            if field_type == bool:
                if value_text == "True":
                    field_value = True
                else:
                    field_value = False
            else:
                field_value = field_type(value_text)

            settings[field_name] = field_value

        QtWidgets.QMessageBox.information(
            self,
            "注意",
            "全局配置的修改需要重启VN Trader后才会生效!",
            QtWidgets.QMessageBox.Ok
        )

        save_json(SETTING_FILENAME, settings)
        self.accept()

6.2 在策略中如何订阅集合竞价tick?

未完待续 ... ...

1. 期货交易结算主要有两种方式

期货交易结算主要有两种方式:

  • 逐笔对冲(不论当日或往日的开仓,就按持仓情况计算)
  • 逐日盯市 (当日和往日的开仓分别计算合计)

2. 逐笔对冲

1、平仓盈亏(逐笔对冲)=开仓价与平仓价之差×手数×交易单位
2、浮动盈亏(持仓盈亏)=当日结算价与开仓价之差×手数×交易单位
3、当日结存(逐笔对冲)=上日结存(逐笔对冲)+ 当日存取合计 + 平仓盈亏 (逐笔对冲)-当日手续费
4、客户权益(逐笔对冲)=当日结存(逐笔对冲)+ 浮动盈亏(持仓盈亏)

3. 逐日盯市

1、平仓盈亏(逐日盯市)=平当日仓盈亏+平历史仓盈亏
(1)平当日仓盈亏=当日开仓价与平仓价之差×手数×交易单位
(2)平历史仓盈亏=平仓价与昨日结算价之差×手数×交易单位
2、持仓盈亏(逐日盯市)=持当日仓盈亏+持历史仓盈亏
(1)持当日仓盈亏=当日结算价与当日开仓价之差×手数×交易单位
(2)持历史仓盈亏=当日结算价与昨日结算价之差×手数×交易单位

4 两种结算方式的特点

4.1 逐日盯市的特点

1、逐日盯市每天都对客户持有的合约进行盈亏计算,并且按照风控水平的设置判断客户账户的风险,如果出现爆仓或者穿仓,实时对用户进行风险通知或者风险处置。
2、因此每天接口只对当前交易日的历史委托单、成交单、当前持仓和账户信息进行推送,如果当前交易日的没有委托、成交操作,那么当前交易日的历史委托单、成交单记录没有任何内容推送。即使有当前持仓,当前交易日之前的历史委托单、成交单也不会被推送给客户端。
3、当前vnpy就是按照大部分接口都支持结算方式,但单逐日盯市更适合交易所和期货公司使用,而非交易者。

4.2 逐笔对冲的特点

1、一笔交易是自从客户对某个合约建仓开始,到该合约的持仓量为零结束;
2、逐笔对冲的成本价会因为加减仓而发生变化;
3、逐笔对冲结算需要记录该笔交易过程中的所有成交单,才能够计算其盈亏曲线,它可以在逐日盯市结算的基础上进行改造得到;
4、可以很好地描述交易策略的交易过程和账户的权益变化情况;
5、更适合交易者和交易策略使用。

1. 交易线的代码

把下面的代码保持为vnpy\usertools\trade_line.py:

'''
Author: hxxjava
Date: 2021-06-24 19:57:32
LastEditTime: 2021-07-05 21:23:04
LastEditors: Please set LastEditors
Description: 
'''
from dataclasses import dataclass
from typing import Callable
from vnpy.trader.constant import Direction,Offset
from vnpy.event import EventEngine,Event
from copy import deepcopy

EVENT_PRICE_CHANGE = "ePriceChange."    # 价格变化消息

@dataclass
class PriceData:
    """
    价格变化数据定义
    """    
    sponsor:str     # 发起者
    price_name:str  # 价格名称
    price:float     # 价格


@dataclass
class TradeCommand:
    """
    交易指令数据定义
    """    
    trade_line_name:str
    sponsor:str       # 发起者  
    price_name:str    # 价格名称
    direction:Direction
    offset:Offset
    price:float
    payup:float
    trade_num:int


class TradeLine():
    """
    名称:交易线。按照设定的条件,发出交易指令。相当于预警下单线。
    """

    def __init__(self,event_engine:EventEngine,tc:TradeCommand,condition:str,on_trade_command: Callable,excuted:bool=False):
        self.event_engine = event_engine
        self.tc = tc
        self.condition = condition
        self.price = None
        self.excuted = excuted
        self.on_trade_command = on_trade_command

        self.register_event()

    def register_event(self):
        """ 注册消息 """
        self.event_engine.register(EVENT_PRICE_CHANGE,self.process_pricechange_event)

    def process_pricechange_event(self,event:Event):
        """ """
        pc:PriceData = event.data
        if self.tc.sponsor == pc.sponsor and self.tc.price_name == pc.price_name:
            self.check_price(pc.price)

    def check_price(self,price:float):
        """ """
        if self.excuted:
            self.price = price
            return

        if self.condition == ">":
            # 大于触发价
            if price > self.tc.price:
                self.send_trade_event(price)

        elif self.condition == "<":
            # 大于触发价
            if price < self.tc.price:
                self.send_trade_event(price)

        elif self.condition == ">=":
            # 大于触发价
            if price >= self.tc.price:
                self.send_trade_event(price)

        elif self.condition == "<=":
            # 大于触发价
            if price <= self.tc.price:
                self.send_trade_event(price)        

        elif self.condition == "^":
            # 上穿触发价
            if self.price is not None:
                if self.price <= self.tc.price < price:
                    self.send_trade_event(price)    

        elif self.condition == "v":
            # 下穿触发价
            if self.price is not None:
                if self.price >= self.tc.price > price:
                    self.send_trade_event(price)  

        self.price = price             

    def send_trade_event(self,price:float):
        """ """
        if self.excuted:
            return

        tc = deepcopy(self.tc) # 简单,不模棱两可
        # tc.price=price  # 注释掉,直接用交易线的价格

        self.excuted = True
        # 执行交易指令
        self.on_trade_command(tc)

    def set_emit_price(self,price:float):
        """ 设置交易线触发价格 """
        self.tc.price = price

    def set_excuted(self,excuted:bool):
        """ 设置交易线是否交易过标志 """
        self.excuted = excuted 

    def reset(self):
        """ 复位交易线 """
        self.price = None
        self.excuted = False 

    def __repr__(self) -> str:
        out_str = "TradeLine=({},condition={},on_trade_command={},price={},excuted={})".\
            format(
                self.tc,
                self.condition,
                self.on_trade_command,
                self.price,
                self.excuted
            )
        return out_str

2. 如何使用交易线

下面是一个测试用的价差交易的策略例子,只为可以演示交易线的创建、触发价格更新和最新价格的更新和交易动作的执行,并不表示该策略可以盈利。
把下面代码保存为boll_arbitrage_strategy.py,保存到vnpy_spreadtrading\stategies目录下,就可以创建价差策略来观察trade_line的运作机制。

""" """
from datetime import datetime
from typing import List,Dict,Tuple,Union
from vnpy.event.engine import Event, EventEngine
from os import close, name, write
from typing import Dict
from vnpy.trader.constant import Direction, Offset,InstrumentStatus
from numpy import nan
import talib
from vnpy.trader.utility import BarGenerator, ArrayManager
from vnpy_spreadtrading.base import LegData
from vnpy_spreadtrading import (
    SpreadStrategyTemplate,
    SpreadAlgoTemplate,
    SpreadData,
    OrderData,
    TradeData,
    TickData,
    BarData
)

from vnpy.usertools.trade_line import (
    EVENT_PRICE_CHANGE,
    PriceData,
    TradeCommand,
    TradeLine
)


class BollArbitrageStrategy(SpreadStrategyTemplate):
    """
    利用Keltner通道进行套利的一种价差交易
    """

    author = "hxxjava"

    bar_window = 5      # K线周期
    boll_window = 26    # BOLL参数1
    max_steps = 4       # 最大开仓次数
    min_step = 2         # 最小开仓距离
    step_pos = 1        # 每步开仓数量     
    payup = 10          
    interval = 5

    spread_pos = 0.0      # 当前价差数量
    boll_mid = nan     # BOLL中轨   
    step = nan         # 开仓步长

    parameters = [
        "bar_window",
        "boll_window",
        "max_steps",
        "min_step",
        "step_pos",
        "payup",
        "interval"
    ]

    variables = [
        "boll_mid",
        "step",
        "spread_pos",
    ]

    def __init__(
        self,
        strategy_engine,
        strategy_name: str,
        spread: SpreadData,
        setting: dict
    ):
        """"""
        super().__init__(
            strategy_engine, strategy_name, spread, setting
        )
        self.event_engine:EventEngine = self.strategy_engine.event_engine

        self.bg = BarGenerator(self.on_spread_bar,self.bar_window,self.on_xmin_spread_bar)
        self.am = ArrayManager(size=60)

        self.pre_spread_pos = self.spread_pos

        self.trade_lines:Dict[str,TradeLine] = {}
        self.init_trade_lines()

        self.bars:List[BarData] = []

    def on_init(self):
        """
        Callback when strategy is inited.
        """
        self.write_log("策略初始化")

        self.load_bar(days=10,callback=self.on_spread_bar)
        # self.load_tick(days=1)

    def on_start(self):
        """
        Callback when strategy is started.
        """
        # 开启响应的所有交易线的交易功能
        self.write_log("策略启动")
        self.adjust_trade_lines_emitprice()
        self.set_trade_lines_excuted("all_lines",False)
        self.adjust_trade_lines_excute()

    def on_stop(self):
        """
        Callback when strategy is stopped.
        """
        self.write_log("策略停止")

        self.put_event()

    def on_spread_data(self):
        """
        Callback when spread price is updated.
        """
        tick = self.get_spread_tick()
        self.on_spread_tick(tick)

    def on_spread_tick(self, tick: TickData):
        """
        Callback when new spread tick data is generated.
        """
        self.bg.update_tick(tick)
        if self.trading:
            pc = PriceData(sponsor=self.strategy_name,price_name=self.spread_name,price=tick.last_price)
            self.event_engine.put(Event(EVENT_PRICE_CHANGE,data=pc))

    def on_spread_bar(self,bar:BarData):
        """
        Callback when 1 min spread bar data is generated.
        """
        # print(f"on_spread_bar bar={bar}")
        self.bg.update_bar(bar)

    def on_xmin_spread_bar(self, bar: BarData):
        """
        Callback when x min spread bar data is generated.
        """
        self.bars.append(bar)
        if len(self.bars) > 100:
            self.bars.pop(0)

        self.am.update_bar(bar)
        if not self.am.inited:
            return

        # self.boll_mid = self.am.ma(self.boll_window)
        self.boll_mid = talib.MA(self.am.close,self.boll_window)[-1]

        # BOLL通道
        std = self.am.std(self.boll_window,array=True) 
        temp = talib.MA(std,5)
        self.step = 2*temp[-1]

        self.adjust_trade_lines_emitprice()

        print(f"boll_mid,step=({self.boll_mid,self.step})")

        self.put_event()

    def on_spread_pos(self):
        """
        Callback when spread position is updated.
        """
        self.spread_pos = self.get_spread_pos()

        if self.spread_pos == 0:
            if self.pre_spread_pos>0:
                # 复位做多交易线和空平交易线交易标志
                self.set_trade_lines_excuted("below_lines",False)

            if self.pre_spread_pos<0:
                # 复位做多交易线和空平交易线交易标志
                self.set_trade_lines_excuted("above_lines",False)

        if self.pre_spread_pos == 0:
            if self.spread_pos>0:
                # 当持有多仓时,使能平多仓交易线
                self.set_trade_lines_excuted("above_mid",False)

            if self.spread_pos<0:
                # 当持有空仓时,使能平多仓交易线
                self.set_trade_lines_excuted("below_mid",False)

        self.pre_spread_pos = self.spread_pos
        self.put_event()

    def init_trade_lines(self):
        """
        init all trade lines
        """
        for step in range(self.min_step,self.min_step+self.max_steps):
            line_name = f"trade_line{step}"
            tc = TradeCommand(trade_line_name=line_name,
                    sponsor=self.strategy_name,
                    price_name=self.spread_name,
                    direction=Direction.SHORT,
                    offset=Offset.OPEN,
                    price=0.0,trade_num=self.step_pos,payup=self.payup)
            trade_line = TradeLine(
                    event_engine = self.event_engine,
                    condition=">=",
                    tc = tc,on_trade_command=self.on_trade_command,
                    excuted=True)
            self.trade_lines[line_name] = trade_line

            line_name = f"trade_line{-step}"
            tc = TradeCommand(trade_line_name=line_name,
                    sponsor=self.strategy_name,
                    price_name=self.spread_name,
                    direction=Direction.LONG,
                    offset=Offset.OPEN,
                    price=0.0,trade_num=self.step_pos,payup=self.payup)
            trade_line = TradeLine(
                    event_engine = self.event_engine,
                    condition="<=",
                    tc = tc,on_trade_command=self.on_trade_command,
                    excuted=True)  
            self.trade_lines[line_name] = trade_line    

        line_name = f"below_mid"
        tc = TradeCommand(trade_line_name=line_name,
                sponsor=self.strategy_name,
                price_name=self.spread_name,
                direction=Direction.LONG,
                offset=Offset.CLOSE,
                price=0.0,trade_num=-1,payup=self.payup)
        trade_line = TradeLine(
                event_engine = self.event_engine,
                condition="v", # 中轨之下
                tc = tc,on_trade_command=self.on_trade_command,
                excuted=False if self.spread_pos < 0 else True)  
        self.trade_lines[line_name] = trade_line  

        line_name = f"above_mid"
        tc = TradeCommand(trade_line_name=line_name,
                sponsor=self.strategy_name,
                price_name=self.spread_name,
                direction=Direction.SHORT,
                offset=Offset.CLOSE,
                price=0.0,trade_num=-1,payup=self.payup)
        trade_line = TradeLine(
                event_engine = self.event_engine,
                condition="^",  # 中轨之上
                tc = tc,on_trade_command=self.on_trade_command,
                excuted=False if self.spread_pos > 0 else True)  
        self.trade_lines[line_name] = trade_line  

    def on_trade_command(self,tc:TradeCommand):
        """
        process trade command
        """ 
        if not self.trading:
            return

        # print(f"excute trade command :{self.get_spread_tick()} {tc}")
        if nan in [self.boll_mid,self.step]:
            return    

        if tc.direction == Direction.LONG: 
            volume = tc.trade_num
            if tc.trade_line_name == "below_mid":  
                # 中轨下全部清空仓
                if not self.spread_pos < 0:
                    self.write_log("无空仓可平!")
                    return
                volume = abs(self.spread_pos) 

            if volume == 0:
                self.write_log("做多0$错误!")
                return

            next_vol = abs(self.spread_pos+volume)
            if not next_vol <= self.max_steps*self.step_pos:
                self.write_log("再做多将超过最大仓位")
                return

            algoid = self.start_long_algo(
                        price=tc.price,
                        volume=volume,
                        payup=tc.payup,
                        offset=tc.offset,
                        interval=self.interval)  

            print(f"executed start_long_algo : {algoid}")

        elif tc.direction == Direction.SHORT:
            volume = tc.trade_num
            if tc.trade_line_name == "above_mid": 
                # 中轨下全部清多仓 
                if not self.spread_pos > 0:
                    self.write_log("无空仓可平!")
                    return
                volume = abs(self.spread_pos) 

            if volume == 0:
                self.write_log("做空0$错误!")
                return

            next_vol = abs(self.spread_pos-volume)
            if not next_vol <= self.max_steps*self.step_pos:
                self.write_log("再做空将超过最大仓位")
                return

            algoid = self.start_short_algo(
                        price=tc.price,
                        volume=volume,
                        payup=tc.payup,
                        offset=tc.offset,
                        interval=self.interval)  

            print(f"executed start_short_algo : {algoid}")

    def adjust_trade_lines_emitprice(self):
        """ 调整各个交易线的触发价格 """
        for step in range(self.min_step,self.min_step+self.max_steps):
            # 调整做空交易线的触发价格 
            line_name = f"trade_line{step}"
            trade_line = self.trade_lines.get(line_name,None)
            if trade_line:
                trade_line.set_emit_price(self.boll_mid+step*self.step)
            # 调整做多交易线的触发价格
            line_name = f"trade_line{-step}"
            trade_line = self.trade_lines.get(line_name,None)
            if trade_line:
                trade_line.set_emit_price(self.boll_mid-step*self.step)

        # 调整上中轨线和下中轨线的触发价格
        for line_name in ["above_mid","below_mid"]:
            trade_line = self.trade_lines.get(line_name,None)
            if trade_line:
                trade_line.set_emit_price(self.boll_mid)        

    def set_trade_lines_excuted(self,select:str,excuted:bool):
        """ 
        设置两套多交易线的执行标志 
        select = "all_lines":设置所有交易线的执行标志
                 "above_lines":设置所有做空交易线的执行标志
                 "below_lines":设置所有做多交易线的执行标志
                 "below_mid":设置平空仓交易线的执行标志
                 "above_mid":设置平多仓交易线的执行标志
        excuted = True:已执行;False:未执行
        """
        for step in range(self.min_step,self.min_step+self.max_steps):
            if select in ["above_lines","all_lines"]:
                # 做空交易线
                line_name = f"trade_line{step}"
                trade_line = self.trade_lines.get(line_name,None)
                if trade_line:
                    trade_line.set_excuted(excuted)

            if select in ["below_lines","all_lines"]:
                # 做多交易线
                line_name = f"trade_line{-step}"
                trade_line = self.trade_lines.get(line_name,None)
                if trade_line:
                    trade_line.set_excuted(excuted)

        if select in ["below_mid","all_lines"]:
            # 下中轨线
            trade_line = self.trade_lines.get("below_mid",None)
            if trade_line:
                trade_line.set_excuted(excuted)   

        if select in ["above_mid","all_lines"]:
            # 上中轨线
            trade_line = self.trade_lines.get("above_mid",None)
            if trade_line:
                trade_line.set_excuted(excuted)   

    def adjust_trade_lines_excute(self):
        """ 调整各个交易线的执行标志 """
        if self.spread_pos == 0:
            self.set_trade_lines_excuted("above_lines",False)
            self.set_trade_lines_excuted("below_lines",False)
            self.set_trade_lines_excuted("above_mid",True)
            self.set_trade_lines_excuted("below_mid",True)
        elif self.spread_pos > 0:
            self.set_trade_lines_excuted("above_lines",True)
            self.set_trade_lines_excuted("above_mid",True)
            self.set_trade_lines_excuted("below_mid",False)
        elif self.spread_pos < 0:
            self.set_trade_lines_excuted("below_lines",True)
            self.set_trade_lines_excuted("above_mid",False)
            self.set_trade_lines_excuted("below_mid",True)      

    def on_spread_algo(self, algo: SpreadAlgoTemplate):
        """
        Callback when algo status is updated.
        """
        if not algo.is_active():
            print(f"algoid = {algo.algoid}")

    def on_order(self, order: OrderData):
        """
        Callback when order status is updated.
        """
        print(f"{self.spread_name} {order}")

    def on_trade(self, trade: TradeData):
        """
        Callback when new trade data is received.
        """
        print(f"{self.spread_name} {trade}")

3 交易线的优点

  • vnpy停止单只能在vnpy的CTA策略模块中使用,而交易线可以在几乎所有的vnpy模块的策略中使用;
  • 交易线的触发交易的价格是可变的,它可以让您的指标具备交易功能,这个在上面的例子中已经做了演示;
  • 和vnpy停止单一样,交易线也是本地的条件单,但它只有符合条件才出发交易动作。

慢体会它用法,您也许会有惊喜发现!

【注:以下的内容来自SFIT的官方文档《综合交易平台结算平台业务操作手册》,上传以下的几个帖子的目的是:通过对《综合交易平台结算平台业务操作手册》中对费率的设置的研究,让大家明白手续费率和保证金率的概念和计算方法,进而正确地计算出交易中发生的手续费和保证金,以及为了计算交易手续费和保证金所需要的参数有哪些。】

3. 费率的设置

3.1 手续费率设置

手续费设置本平台分为交易所手续费率设置和投资者手续费率设置,主要是对期货合约的手续费率按照交易所规定和公司要求进行设置,分为开仓、平仓、平今、结算、交割、移仓等6种手续费率,其中结算、移仓手续费率并未启用,不需要设置。每种手续费率,以按金额、按手数等2种方式取和收取手续费。记按金额手续费率为R金额、按手数手续费率为R手数,手续费C手续费的计算公式如下:

C手续费=R金额×成交金额+R手数×成交手数

交易所手续费率设置:

对于新合约的上市,需要设置交易所手续费率,在flex柜台中进行新增、修改、删除、查询操作,交易所手续费设置,主要用于设置交易所手续费率,以计算每笔成交的上交手续费,分为按产品和合约设置,同一产品和该产品的合约,合约优先级大于产品。在结算柜台中的“手续费率”目录下,“交易所手续费率设置”“交易所手续费率查询”菜单可以进行交易所手续费率的新增、修改和查询操作。手续费率设置后重新结算,就按新设置的手续费率计算手续费,盘中设置手续费率是否实时上场。

Flex柜台界面操作:

“费率设置->手续费率->交易所手续费率设置->新增即可”
查询:选择交易所、产品/合约,若均为空的话,则是对所有产品和合约进行查询;
新增:选择交易所;选择产品/合约,合约的优先级高于产品;对开仓、平仓、平今、结算、交割四个手续费率进行添加,结算、移仓手续费率并未启用,不需要设置,按金额、按手数等2种方式取和收取手续费;
修改:选中需要修改的记录,点击“修改”按钮,进入“交易所手续费率(修改)”界面,可以修改费率,不能修改交易所和产品/合约;
删除:选中需要删除的记录,即可进行删除操作。

投资者手续费率的设置

投资者手续费设置,主要用于设置投资者手续费率,以计算每笔成交的投资者手续费。在结算柜台中的“手续费率”目录下,“投资者手续费率设置”菜单可以进行投资者手续费率的新增、修改和查询操作。投资者手续费率分为公司标准、模板、单一投资者,按照所述对象范围的粒度大小,这3种情况对应的保证金率设置存在如下的优先级关系:

    单一投资者>模板>公司标准

系统按照投资者范围优先级的先后关系,三种情况设置分为按品种和按合约。对某一个投资者手续费率生效的优先级别是单一投资者>手续费率模板>公司标准,同一产品和该产品的合约,合约优先级别大于产品,系统依次去寻找投资者的手续费率设置,直到找到相应设置为止。
手续费率设置后重新结算,就按新设置的手续费率计算手续费,盘中设置手续费率是实时上场。手续费率实时同步的设置在flex柜台界面的操作流程:
“费率设置->手续费率盘中同步参数设置->选择“是”->点击确认”。
针对投资者手续费率设置中,分为三种情况,一、设置所有投资者手续费率,二、设置手续费率模板手续费率,三、 设置单一投资者手续费率,单一投资者设置与公司标准设置相对比较简单,下面主要介绍一新开户投资者模板手续费率设置步骤。

对于新开户投资者手续费率设置步骤(模板):

  1. 设置投资者手续费率之前,要进行一些参数设置;
    ➢ 投资者手续费率设置是否启用复核流程,若启动复核流程,设置投资者手续费率时候要进行复核操作,复核通过之后手续费率设置才生效;
    Flex柜台界面操作:
    “流程管理->复核流程管理->流程ID选中投资者手续费率设置->查询”,
    “选中记录->修改(流程ID不允许修改) ->保存”
    是否启用:“是/否”
    最高复核级别:“零级复核”即不需要符合;“一级复核”即需要一次复核;“二级复核”需要复核两次;
    是否允许自复核:“是/否”,即选择一级、二级复核是可否由修改手续费的操作员自己进行复核。
    ➢ 投资者手续费率模板对应关系设置是否启动复核流程,若启动复核流程,设置投资者手续费率模板对应关系时候要进行复核操作,复核通过之后投资者手续费率模板对应关系设置才生效;
    Flex柜台界面操作:
    “流程管理->复核流程管理->流程ID选中投资者手续费率模板关系设置->查询”,选中记录->修改(流程ID不允许修改) ->保存
    是否启用:“是/否”
    最高复核级别:“零级复核”即不需要符合;“一级复核”即需要一次复核;“二级复核”需要复核两次;
    是否允许自复核:“是/否”,即选择- -级、二级复核是可否由修改手续费的操作员自己进行复核。
    ➢ 手续费率模板数据权限是否启用,若启用,则操作员只能操作和自己有关联关系的手续费率模板;若不启用,操作员可以操作所有手续费率模板;
    Flex柜台界面操作:
    “交易管理->基本参数设置->结算参数->手续费模板数据权限->启动”
  2. 创建投 资者手续费率模板;
    在实际业务操作中,为每一个投资者都单独设置一套 手续费率是不明智的。为了便于管理和操作的简洁性,手续费率设置中可以采用手续费率模板形式为投资者设置手续费率,以简化操作,故需要先创建一个模板。
    Flex柜台界面操作:
    “费率设置->手续费率->手续费率模板设置->新增(填写手续费模板代码、手续费模板名称及备注) ->点击确认
    查询:输入代码可以精确查询,“空”则为查询全部,同时在左下角可以查询模板手续费率及模板对应投资者;
    新增:添加手续费率模板;
    修改:选中所需修改手续费率模板,点击“修改”按钮,只能修改手续费率模板名称及备注;
    删除:选中所需修改手续费率模板,点击“删除”按钮。
  3. 建立操作 员和手续费率模板对应关系;
    模板创建完成之后,需要将某一操作员与此模板建立起对应关系,即此模板只有该操作员有权限。
    Flex柜台界面操作:
    “费率设置->手续费率->操作员对应手续费率模板权限管理->点击新增(填写之前创建的手续费率模板、操作员代码) ->点击确认”
    查询:选择手续费率模板或操作员代码,点击“查询”,若为“空”则查询所有模板;
    新增:添加手续费率模板与对应操作员之间的关系;
    删除:选中记录,点击“删除”按钮即可。
  4. 建立投资者手续 费率模板和投资者对应关系;
    需要将投资者归入一一个手续费率模板,这样投资者的手续费率会跟随该模板进行变动,且一个投资者只可以属于一个手续费率模板。
    Flex柜台界面操作:
    “费率设置->手续费率->投资者手续费率模板对应关系->点击新增(填写之前创建的手续费率模板、投资者代码) ->点击确认’
    查询:可以按照投资者代码、手续费率模板、投资者属性进行查询,若为空,则查询全部;
    新增:添加投资者手续费率模板对应关系;
    修改:只能修改手续费率模板字段,不能修改投资者代码;
    删除:选中记录,点击“删除”按钮即可;
    批量新增:新开户的同一属性投资者可以同时建立投资者手续费率模板对应关系;
    批量修改:同一属性投资者可以实现同步修改手续费率模板;
    批量删除:查询到的投资者与模板对应关系可以实现同时删除。
  5. 设置模板手续费率;
    在flex柜台中进行新增、修改操作,该项内容设置完成后就实现了对同一属性的投资者通过模板进行手续费率的设置。
    Flex界面操作流程:
    “费率设置->手续费率->模板手续费率设置->新增->填写费率模板、产品/合约、选择交易所->添加开仓、平仓、平今、交割手续费率(按金额和按手数) ->点击确认按钮”
    查询:可以按照费率模板、交易所进行查询,若为空,则查询全部;
    新增:添加模板手续费率的设置;
    修改:只能修改相关费率字段,不能修改费率模板、交易所、产品/合约;
    删除:选中记录,点击“删除”按钮即可;
    批量删除:可以对查询到的记录进行全部删除;
    复制:分为投资者复制和交易所复制,投资者复制将某一投资的手续费率设置复制到所创建的模板中:交易所产品复制是将所创建模板中某交易所的产品/合约复制到某交易所的相关产品/合约中。
    当然,以上过程的设置也可以通过“投资者手续费率设置->新增-> 投资者范围选模板->填写费率模板、产品/合约、选择交易所->添加开仓、平仓、平今、交割手续费率(按金额和按手数) ->点击确认按钮”来实现;
    按照模板手续费率设置流程,对于单一投资者和公司标准可以分别通过“投资者手续费率设置”和“单一投资者手续费率设置”来实现,步骤同上。
  6. 设置完成之后可以到“投资者费率查询”里进行确认投资者的手续费率设置是否正确;在“手续费率修改查询”里能查询修改记录。

新功能——手续费率模型:

对投资者手续费率进行批量调整时,可以通过手续费率模型来实现。可以通过创建一个模型A实现对当前投资者手续费率数据进行备份,同时将数据复制到一个新模型B,如果手续费进行批量调整时可以对模型B进行批量修改,激活启用,那么投资者手续费率即按照模型B来进行收取;若过段时间手续费率需要进行恢复以前设置,则激活模型A即可。
手续费率模型应用步骤如下:

  1. 创建投资者手续费率模型;
  2. 设置手续费率模型的相关费率;
  3. 对所创建模型进行激活操作。

在flex中界面操作:
▶“ 费率设置->手续费率模型管理->手续费率模型-> 点击新增->填写手续费率模型代码、名称、选择交易所和产品、备注->从当前数据创建或确认”
注:若点击“确认”,手续费率需要到“模型投资者手续费率”中进行添加设置,添加过程中是与所创建模型是相对应的,包括:交易所得选择、产品/合约的选择;若是“从当前数据创建”,则所创建模型即为当前手续费率数据的设置;注:当交易所为空时,表示该手续费率模型的适用范围为所有交易所所有产品,激活时将覆盖所有手续费率;创建模型时,当交易所不为空,产品为空时,表示该手续费率模型的适用范围为特定交易所所有产品,激活时将覆盖特定交易所的所有产品;当交易所、产品都不为空时,表示该手续费率模型的适用范围为特定交易所特定产品,激活时将覆盖特定交易所特定产品的手续费率。
▶“费率设置-> 手续费率模型管理->模型手续费率->新增->填写模型代码、选择交易所、产品/合约、投资者范围、添加开仓、平仓、平今、交割手续费率(按金额和按手数) ->点击确认按钮”
查询:可以按照模型代码、交易所、产品/合约、投资者范围、组织架构进行查询,若为空,则查询全部;
新增:添加模型手续费率的设置;
修改:只能修改相关费率字段,不能修改费率模板、交易所、产品/合约;
删除:选中记录,点击“删除”按钮即可;
批量修改:可以实现对公司范围、模板、单一投资者进行批量修改,可分为:绝对调整(调整后的值为当前调整)和相对调整(调整后的值为调整值加上原有值),调整完成后,可以进行试算操作,检查是否正确,若正确则点击确认按钮。
批量删除:可以对查询到的记录进行全部删除;
复制:是将一个手续费率模型复制到另一个手续费率模型,可以按照投资者范围、交易所的产品/合约进行复制,则目标的模型手续费率设置将被删除、替换为源的模型手续
费率。
▶在“手续费率模型”界面中,选中所需要模型,点击“激活”即可,这样就实现对一个新建模型的启用。
▲交易所手续费率、投资者手续费率设置都是实时生效的。

条件单规则

1. 简介

条件单是一个带有触发条件的指令。该触发条件可以以市场上的最新行情为基准,也可以以指定价格为基准。比如:一个投资者有1手IF1910的空头持仓,并希望在市场价低于2200时买入平仓,他就可以使用条件单。这样当行情波动到满足该条件时,该报单就会被自动触发报出,而不需要他本人时刻盯着电脑屏幕去监视市场行情。

申报条件单时,条件单的报单回报中OrderSysID是以字符串“TJBD_”开头的由CTP系统自定义的报单编号,针对该交易所当日唯一;只对成功申报的条件单编号,错单不编号。因为这种情况下的编号由CTP系统自行维护,与交易所无关,仅用于管理本系统内的该条件单。
有效的使用条件单,可以做出限价止损指令(stop-and-limit order)和触及市价指令(market-if-touched order)。
CTP条件单为CTP后台系统自带指令,并非交易所官方支持指令。

2.指令介绍

报入条件单指令使用ReqOrderInsert函数
其中的核心数据结构是CThostFtdcInputOrderField
在结构体中和普通报单有区别的字段如下:

    ///触发条件
    TThostFtdcContingentConditionType   ContingentCondition;
    ///止损价
    TThostFtdcPriceType StopPrice;

条件单有效的触发条件如下:

    ///最新价大于条件价
    #define THOST_FTDC_CC_LastPriceGreaterThanStopPrice '5'
    ///最新价大于等于条件价
    #define THOST_FTDC_CC_LastPriceGreaterEqualStopPrice '6'
    ///最新价小于条件价
    #define THOST_FTDC_CC_LastPriceLesserThanStopPrice '7'
    ///最新价小于等于条件价
    #define THOST_FTDC_CC_LastPriceLesserEqualStopPrice '8'
    ///卖一价大于条件价
    #define THOST_FTDC_CC_AskPriceGreaterThanStopPrice '9'
    ///卖一价大于等于条件价
    #define THOST_FTDC_CC_AskPriceGreaterEqualStopPrice 'A'
    ///卖一价小于条件价
    #define THOST_FTDC_CC_AskPriceLesserThanStopPrice 'B'
    ///卖一价小于等于条件价
    #define THOST_FTDC_CC_AskPriceLesserEqualStopPrice 'C'
    ///买一价大于条件价
    #define THOST_FTDC_CC_BidPriceGreaterThanStopPrice 'D'
    ///买一价大于等于条件价
    #define THOST_FTDC_CC_BidPriceGreaterEqualStopPrice 'E'
    ///买一价小于条件价
    #define THOST_FTDC_CC_BidPriceLesserThanStopPrice 'F'
    ///买一价小于等于条件价
    #define THOST_FTDC_CC_BidPriceLesserEqualStopPrice 'H'

条件价对应的字段为StopPrice

1. 什么是TAS指令?

结算价交易(Trading at Settlement,以下简称TAS)指令,允许交易者在规定交易时段内按照期货合约当日结算价或当日结算价增减若干个最小变动价位申报买卖期货合约。

2. TAS指令撮合原的则是什么?

TAS指令仅可与同一合约的TAS指令撮合成交。在集合竞价采用最大成交量原则进行撮合,在连续竞价交易时段采用价格优先、时间优先原则进行撮合。

3. 为什么要推出TAS指令?

TAS指令已在国际成熟市场得到广泛使用,其本质是为市场提供了一种便捷高效的风险管理工具,从而对促进市场功能发挥、改善市场投资者结构、提升价格影响力等起到积极作用。
对于实体企业,在进行现货贸易点价时,多采用期货合约结算价作为定价基准。在利用期货市场进行风险对冲时,如果没有这个工具,一般要通过盘中多次下单来模拟当日结算价,因此在交易执行方面存在较高的难度,且套保效率较低。
TAS指令可以在规定的交易时段内以结算价或结算价附近的价格进行交易,大大降低了企业围绕结算价交易的不确定性。企业可利用TAS指令更便利、更精准地做好风险管理。
从成熟市场普遍情况看,除了实体企业利用TAS指令进行风险管理,ETF和长期策略基金等机构投资者也运用TAS指令来进行换月移仓交易等,这有效降低了因机构头寸大量变化对市场短期价格的冲击。因此,TAS指令的推出能够促进市场主体有效改善利用结算价进行套保交易的执行效率,吸引境内外多元化市场主体的积极参与,进一步完善投资者结构,有益于市场稳定运行。同时有利于交易者更为便利和广泛地参考中国期货价格,从而提升市场的价格影响力。

1. 升级后进入回测,界面如下:

description

2. 下载数据时遇到下面的错误

11:11:22 初始化CTA回测引擎
11:11:22 策略文件加载完成
11:12:26 ----------------------------------------
11:12:26 rb2201.SHFE-1m开始下载历史数据
11:12:26 数据下载失败,触发异常:
Traceback (most recent call last):
File "D:\ProgramFiles\VnStudio\lib\site-packages\vnpy_ctabacktester\engine.py", line 404, in run_downloading
data = self.datafeed.query_bar_history(req)
File "D:\ProgramFiles\VnStudio\lib\site-packages\vnpy_rqdata\rqdata_datafeed.py", line 164, in query_bar_history
adjust_type="none"
File "D:\ProgramFiles\VnStudio\lib\site-packages\rqdatac\decorators.py", line 150, in wrap
return func(args, **kwargs)
File "D:\ProgramFiles\VnStudio\lib\site-packages\rqdatac\services\get_price.py", line 103, in get_price
order_book_ids, market
File "D:\ProgramFiles\VnStudio\lib\site-packages\rqdatac\services\get_price.py", line 222, in classify_order_book_ids
ins_list = ensure_instruments(order_book_ids, market=market)
File "D:\ProgramFiles\VnStudio\lib\site-packages\rqdatac\validators.py", line 168, in ensure_instruments
all_instruments = _all_instruments_dict(market)
File "D:\ProgramFiles\VnStudio\lib\site-packages\rqdatac\decorators.py", line 129, in wrapper
value = user_function(
args, kwargs)
File "D:\ProgramFiles\VnStudio\lib\site-packages\rqdatac\services\basic.py", line 139, in _all_instruments_dict
ins = _all_cached_instruments_list(market)
File "D:\ProgramFiles\VnStudio\lib\site-packages\rqdatac\decorators.py", line 129, in wrapper
value = user_function(*args,
kwargs)
File "D:\ProgramFiles\VnStudio\lib\site-packages\rqdatac\services\basic.py", line 134, in _all_cached_instruments_list
return _all_instruments_list(market)
File "D:\ProgramFiles\VnStudio\lib\site-packages\rqdatac\services\basic.py", line 112, in _all_instruments_list
ins = [Instrument(i) for i in get_client().execute("all_instruments", market=market)]
File "D:\ProgramFiles\VnStudio\lib\site-packages\rqdatac\client.py", line 31, in execute
raise RuntimeError("rqdatac is not initialized")
RuntimeError: rqdatac is not initialized

3. 好像是说米筐数据没有初始化?

虽然说是整个删除以前的版本,然后重新安装的2.6版本,但是以前的设置一点也没有改变,难道是以前的米筐接口设置不可以在新版本中继续使用了吗?

CTP接口之保证金率手续费率查询中我们已经讲过如何查询保证金率和手续费率,这一篇再详细讲一下根据费率计算相应的保证金,为最终资金权益等的计算做铺垫。

一、保证金计算公式

保证金的计算公式如下:
保证金 = 按手数保证金费*手数+按金额保证金率*金额*手数*合约乘数=(按手数保证金费+按金额保证金率*金额*合约乘数)*手数
按照API中的字段即是:
多头保证金=(LongMarginRatioByVolume + LongMarginRatioByMoney * Price *VolumeMultiple )* Volume
空头保证金类似。
这个公式中按手数保证金费和按金额保证金率可以通过ReqQryInstrumentMarginRate查询得到;合约乘数可以通过ReqQryInstrument查询得到;手数是涉及到计算到的手数。那就只剩下一个问题,用什么金额去计算?

二、保证金价格类型

用什么金额去计算客户的保证金,对于昨仓统一使用昨结算价计算,对于今仓,在期货公司后台是可以设置的。客户也可以通过ReqQryBrokerTradingParams(请求查询经纪公司交易参数)查询得到设置的类型。查询示例如下:

qryField = api.CThostFtdcQryBrokerTradingParamsField()
qryField.BrokerID="8000"
qryField.InvestorID="012345"
qryField.CurrencyID = "CNY"
tradeapi.ReqQryBrokerTradingParams(qryField,0)

在该查询的回调结果中查看MarginPriceType枚举值:

def OnRspQryBrokerTradingParams(self, 
    pBrokerTradingParams: 'CThostFtdcBrokerTradingParamsField', 
    pRspInfo: 'CThostFtdcRspInfoField', 
    nRequestID: 'int', bIsLast: 'bool') -> "void":    
    if pBrokerTradingParams is not None:                            
        print(f"MarginPriceType:{pBrokerTradingParams.MarginPriceType}")

枚举值列表如下:

///昨结算价
#define THOST_FTDC_MPT_PreSettlementPrice '1'
///最新价
#define THOST_FTDC_MPT_SettlementPrice '2'
///成交均价
#define THOST_FTDC_MPT_AveragePrice '3'
///开仓价
#define THOST_FTDC_MPT_OpenPrice '4'

可见计算保证金中用到的价格类型有4种。

下面我们举例说明,设au2106昨结算价为391.74,查询合约得到合约乘数为1000,查询保证金率得到LongMarginRatioByMoney为0.08,LongMarginRatioByVolume为0.0,ShortMarginRatioByMoney为0.08,ShortMarginRatioByVolume为0.0。

三、保证金的计算

01 以昨结算价计算为例

目前simnow即采用昨结算价计算保证金。如果上述查询返回得到参数MarginPriceType为1,则说明CTP后台使用昨结算价计算保证金。
此时假设以396.16报买开1手,未成交,CTP会冻结保证金,账户的可用资金会减少。这时,
冻结保证金=(按手数保证金费+按金额保证金率*昨结算价*合约乘数)*手数=(0.08*391.74*1000)*1=31339.20。
经过一段时间 ,该买开仓以396.16成交,此时客户有1手多头持仓,相应的冻结保证金减少,增加占用保证金。这时,
冻结保证金=0占用保证金=(按手数保证金费+按金额保证金率*昨结算价*合约乘数)*手数=(0.08*391.74*1000)*1=31339.20。
在整个交易日内,au2106合约的行情最新价格上上下下不停变化,这时客户该合约上的持仓盈亏也不断变化,但是这1手占用的保证金因为是用昨结算价计算的,昨结算价不变,占用保证金也不变。
等到第二个交易日,该合约有新的结算价时,占用保证金才发生相应的变化。

02 其他价格类型

如果上述查询返回得到参数MarginPriceType为2,则说明CTP后台是以行情中最新价计算保证金的。
这时冻结保证金和占用保证金均是使用最新 价,而且会随着最新价的更新而动态变化。其他的价格类型也是类似计算,按成交均价算则是按照行情里面的均价更新,按开仓价算则是冻结时用报单价,成交后用成交价计算。
只有在最新价和成交均价这两种情况下今仓的保证金值才会随着行情波动而更新,昨仓的保证金值一直是用昨结算价计算得到,不会改变。

四、保证金计算其他注意项

01 保证金优惠

为了增加资金可用率,交易所都有一定的保证金优惠政策。例如上期所的品种大额单边保证金优惠,是指同一品种内所有的合约按多空分别累计保证金,然后实际只收大额一边的保证金,因为同品种多空有一定风险对冲,同时收两边保证金过于保守。

举例:

客户总资金为200,000,此时au2106多头占用保证金100,000,au2102空头占用保证金80,000,根据品种大单边优惠规则,CTP后台实际算占用保证金为100,000,也就是说客户现在可用资金为200,000-100,000=100,000。
这时客户又成交1手au2104的空头,占用保证金为30,000,总的空头占用保证金为110,000,大于多头的100,000。所以CTP后台实际占用保证金为110,000,客户可用资金为200,000-110,000=90,000。
品种是否采用大额单边保证金算法可以根据查合约ReqQryInstrument请求得到返回中的MaxMarginSideAlgorithm来判断。当下的大额单边保证金占用多少,可以通过ReqQryInvestorProductGroupMargin查询得到。
除了上期所品种内的大额单边保证金优惠外,中金所还有跨品种的大额单边保证金优惠,例如对股指类期货:沪深300股指期货、上证50股指期货和中证500股指期货这三个的跨品种双向持仓,按照交易保证金单边较大者收取交易保证金;对国债类期货也实施跨品种单向大边保证金制度。

大商所郑商所对普通合约还是按照双边收取保证金,但对于标准套利合约只收两个保证金中较高的,大商所盘中申请组合成功也有大边保证金优惠。

02 保证金率调整

保证金率在整个合约存在过程中并不是一直不变,常见的调整保证金情况有:

  • 节假日前为了规避国际市场的风险调整保证金;
  • 单边市出现停板情况时调整保证金;
  • 出现极端风险或市场震荡时调整保证金;
  • 持仓量达到一定数量是交易所调整保证金;
  • 临近交割月调整保证金;
  • 交易所规定的其他情况。

03 市价单保证金计算

对于市价单,在计算保证金冻结时,使用涨跌停价计算,计算占用保证金时,按照价格类型采用相应的价格计算。

保证金率和手续费率都是程序化交易者非常关心的话题,保证金率涉及到开仓时账户资金的冻结和占用,撤单或平仓则会解冻和解占用。手续费率则是真正要付给期货公司的佣金,而且通常开仓,平昨和平今手续费还各有不同。
这里面各种各样的坑,比如有人反应期货公司有的按交易所保证金率收,有的按交易所两倍来收,这样同样的钱在有的期货公司够开2$,而在有的就只能开1手;手续费那更是收的五花八门,各种各样的都有。甚至还有人反应出现过实际收的和开户时说的不一样这种事。
其实这两个费率都是可以通过CTP API直接查询得到的,但这两个查询和其他查询(如查询合约,资金,结算单等)有点不一样,所以本篇单独讲一下。

一、 查询保证金率

官方文档中涉及到保证金率的接口好几个,新手开发看到经常一脸懵逼,不知道究竟哪个才是实际中用到的。这里我们先罗列一下相关的:

//请求查询合约保证金率
def ReqQryInstrumentMarginRate(self, 
    pQryInstrumentMarginRate: 'CThostFtdcQryInstrumentMarginRateField', 
    nRequestID: 'int') -> "int"
//查询合约的返回中会带每条合约的保证金率
def ReqQryInstrument(self, 
    pQryInstrument: 'CThostFtdcQryInstrumentField', 
    nRequestID: 'int') -> "int"
//请求查询交易所保证金率
def ReqQryExchangeMarginRate(self, 
    pQryExchangeMarginRate: 'CThostFtdcQryExchangeMarginRateField', 
    nRequestID: 'int') -> "int"
//请求查询交易所调整保证金率
def ReqQryExchangeMarginRateAdjust(self, 
    pQryExchangeMarginRateAdjust: 'CThostFtdcQryExchangeMarginRateAdjustField',     
    nRequestID: 'int') -> "int"

在实际交易中冻结和占用的计算其实是采用第一个查询函数ReqQryInstrumentMarginRate的返回结果。
而ReqQryInstrument中返回的保证金率通常是交易所保证金率,计算中并不用到。ReqQryExchangeMarginRate和ReqQryExchangeMarginRateAdjust查询得到的是交易所相关的保证金率,这也只在计算中间过程中会用到。
这里我们只讲实际计算中用到的ReqQryInstrumentMarginRate查询,其参数类型为CThostFtdcQryInstrumentMarginRateField,具体字段:

01必填字段

BrokerID //经纪公司代码
InvestorID //投资者代码
HedgeFlag //投机套保标志

前两者开户时可得到,投机套保标志一般投资者填投机就可以。注意这三个字段是一定要填的,不填的话返回值就为空。

02选填字段

InstrumentID //合约代码

如果InstrumentID填空,则返回客户当前持仓对应的合约保证金率,否则返回相应InstrumentID的保证金率。也就是说这个查询和其他查询不一样,如果什么都不填,是不会得到所有的合约保证金率的。
最后给出个查询保证金率的示例:

qryinfofield = api.CThostFtdcQryInstrumentMarginRateField()
qryinfofield.BrokerID='9999'
qryinfofield.InvestorID='000001'
qryinfofield.InstrumentID='rb2007'
qryinfofield.HedgeFlag = api.THOST_FTDC_HF_Speculation
tapi.ReqQryInstrumentMarginRate(qryinfofield,0)

二、 查询手续费率

查手续费的接口也有好几个,罗列如下:

//请求查询合约手续费率
def ReqQryInstrumentCommissionRate(self, 
    pQryInstrumentCommissionRate: 'CThostFtdcQryInstrumentCommissionRateField',     
    nRequestID: 'int') -> "int":
//请求查询报单手续费    
Def ReqQryInstrumentOrderCommRate(self, 
    pQryInstrumentOrderCommRate: 'CThostFtdcQryInstrumentOrderCommRateField',
     nRequestID: 'int') -> "int":    
// 请求查询期权或做市商合约手续费函数这里就不罗列了...

在期货交易中,实际计算用到的只有个前面两个函数,其中第一个查询的是合约的手续费率,第二个查询的是报单申报手续费(就是每报撤一次单子都会收一笔费用),目前来说这是中金所特有的。

也就是说中金所的手续费分为两部分,包括交易手续费和申报费两部分,前者可以通过第一个查询函数查到,后者通过第二个查询函数得到。
两个查询函数的参数类型虽然不一样,但是字段填写方法是一致的,这里我们以ReqQryInstrumentCommissionRate为例讲解,参数类型为CThostFtdcQryInstrumentCommissionRateField,具体字段:

01必填字段

BrokerID //经纪公司代码
InvestorID //投资者代码

02选填字段

InstrumentID //合约代码

如果InstrumentID填空,则返回客户当前持仓对应的合约手续费率,否则返回相应InstrumentID的合约手续费率。和上面查询保证金率的逻辑一样。

查询结果返回值还需要注意几点:

  1. 以开仓为例,查询手续费返回结果中有两项关于开仓的字段:OpenRatioByMoney(开仓手续费率)和OpenRatioByVolume(开仓手续费),这是两种计算手续费的方法。实际计算时的开仓手续费公式为:

手续费 = 成交数量*(成交价*合约乘数*OpenRatioByMoney+OpenRatioByVolume)

  1. 有时查询某合约(例如IF2009)的手续费时,得到的是其对应品种(例如IF)的手续费。这是因为该品种下所有合约的手续费都是一样的,所以就直接将手续费设到品种级别了。
  2. 交易所的手续费率这里查不到,大家可以去交易所官网上看,通常在交易数据/结算参数中。要注意的是大商所手续费中有个短线开平仓的概念,其实就是指今开今平。

最后给出个查询手续费率的示例:

qryinfofield = api.CThostFtdcQryInstrumentCommissionRateField()
qryinfofield.BrokerID='9999'
qryinfofield.InvestorID='000001'
qryinfofield.InstrumentID='rb2007'
tapi.ReqQryInstrumentCommissionRate(qryinfofield,0)

【来自CTP期货期权API接口说明.chm】
CTP交易系统基于安全和性能考虑,在诸多地方有流量控制,其中流量控制又分FTD报文流量控制、报单流量控制、查询流量控制等。而这些流量控制分布在各个不同的地方。此处将会给大家详细介绍。

1.报单流控

报单流控是指用户在本交易系统报单(ReqOrderInsert)、撤单(ReqOrderAction)时每秒内允许的最大笔数。
报单流控限制配置在CTP柜台端【程序化交易频繁报撤单管理】菜单。
如果超过这个限制API会通过OnRspOrderAction提示:“CTP:下单频率限制”。

2.查询流控

查询流控是指用户当前Session在做查询的时候每秒内允许的最大请求笔数。
查询流控配置在交易前置组件上,配置项为【QryFreq】。其中API内置了在途查询流控1笔。
自从穿透式监管版本以后,API在连接交易前置时,会去查询到前置的查询流控设置(该设置配置在front_se组件)。假设前置配置了2笔/秒,那么连接该前置的API每秒只能发# 起2笔查询请求。
但是要注意,不管怎么配置,API都内置了在途流控,在途查询流控为1笔。即,当前这笔查询请求发出后,在未收到响应前,不能发起下一笔查询请求。
在过去,查询流控是内置在API里,1笔每秒,在途1笔。
如果超过交易前置配置的查询流控,则会触发OnRspError,并提示:“CTP:查询未就绪,请稍后重试”
如果超过API内置的在途流控,则查询请求的返回值为-2,表示未处理请求超过许可数。
所有ReqQuery开头查询函数不受流控限制,原因是此类函数都是通过交易核心处理的,不通过查询核心。

3.FTD报文流控

FTD报文流控是指用户当前Session在提交API指令的时候每秒内允许的最大请求笔数。
FTD报文流控配置在交易前置组件上,配置项为【FTDMaxCommFlux】。
FTD报文流控是一种综合性的流控手段,API的所有接口在跟前置交互的时候都是使用FTD协议的报文进行通讯的,因此这就意味着,登录、查询、报单、撤单和报价等等所有请求都在FTD报文流控的限制范围内。
例如如果配置为6,可以简单理解为用户仅能调用6次API请求指令,包括登录、查询、报单撤单等等。
如果超过FTD报文流控,则超过的指令会被缓存到前置,直到下一秒发出。例如用户在一秒内报单10笔,则前6笔会立即发送给核心,而后4笔则会被缓存在前置,到下一秒才发出。因此对于用户程序端的感受就是后4笔报单延迟很大,实则是受到了流控。
注意,FTD报文流控不会有错误信息或错误返回。

4.前置连接数流控

前置连接数流控是指在本交易前置对同一IP每秒允许的最大API连接请求数。
前置连接数流控配置在交易前置组件上,配置项为【ConnectFreq】。
需要注意的是,API连接请求指的是API从init到OnFrontConnected的过程,跟登录无关。可以简单理解为一次init就是一个连接请求。
例如如果配置为20,则一秒内最多有20个session建立跟前置的连接。
如果超过前置连接数流控则会被主动断开连接,触发OnFrontDisconnected。因此用户如果发现自己的程序一连接前置就被断开,则除了版本问题外,有可能是遇到了连接数流控。

5.同一用户最大允许在线会话数

同一用户最大允许在线会话数是指同一个用户(UserID)在本交易系统中同时登录在线的最大允许会话数。
同一用户最大允许在线会话数配置在柜台或交易核心中。
注意,这个会话数针对的是本交易系统,而非单一前置。
如果超过同一用户最大允许在线会话,则会通过OnRspUserLogin返回“CTP:用户在线会话超出上限”的错误。

6.交易所API流控

交易所API流控指通过交易所API发送报单等请求的每秒最大允许数。
交易所API流控的阈值设置在交易所端,由交易所API查询获取,该流控实际控制在交易所API端。
受到交易所流控后会触发OnRtnOrder,报“CTP:交易所每秒发送请求数超过许可数”或者“CTP:交易所未处理请求超过许可数”。

© 2015-2022 微信 18391752892
备案服务号:沪ICP备18006526号

沪公网安备 31011502017034号

【用户协议】
【隐私政策】
【免责条款】