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

1. vnpy既可以手动下单,也可以自动下单

1.1 vnpy处理CTA交易的大致过程:

  1. 系统启动的过程中,OmsEngine注册系统委托单、成交单和持仓消息处理函数
  2. 手动或者自动发起CTP接口的委托申请,等待CTP接口的请求响应
  3. CTP接口接受委托申请,推送接受的委托单,OmsEngine保存并且显示委托单
  4. 委托单状态发生变化时,推送状态变化的委托单,OmsEngine保存并且显示委托单
  5. 委托单一旦成交,CTP接口推送成交单数据,OmsEngine保存并且显示成交单
  6. CTP接口周期性(4秒)推送成持仓数据,OmsEngine保存并且显示持仓

1.2 既可以手动下单,也可以在自动下单

Main_Window可以手动下单,也可以在StrategyManager中启动用户策略自动下单。这些下单动作无论怎么变化,都大致经过上述的过程。

1.3 不同的策略也可以同时交易相同的合约

如果用户使用不同的CTA策略,交易相同的合约,那么在当前的主界面(Main_Window)中它们如何区分:哪一笔委托单是属于哪一个策略的?哪一笔成交单是属于哪一个策略的?

1.4 目前vnpy没有建立委托单、成交单与策略的关系

也就是说你看到主界面(Main_Window)中有一个成交单,你无法确定它是手动下单的还是自动下单的,如果有不同策略交易同一合约,你可能也无法确定是属于哪个策略的。

2. 这样修改就可以

2.1 看看cta_strategy\engine.py

    def send_server_order(
        self,
        strategy: CtaTemplate,
        contract: ContractData,
        direction: Direction,
        offset: Offset,
        price: float,
        volume: float,
        type: OrderType,
        lock: bool
    ):
        """
        Send a new order to server.
        """
        # Create request and send order.
        original_req = OrderRequest(
            symbol=contract.symbol,
            exchange=contract.exchange,
            direction=direction,
            offset=offset,
            type=type,
            price=price,
            volume=volume,
        )

        # Convert with offset converter
        req_list = self.offset_converter.convert_order_request(original_req, lock)

        # Send Orders
        vt_orderids = []

        for req in req_list:
            req.reference = strategy.strategy_name      # Add strategy name as order reference

            vt_orderid = self.main_engine.send_order(
                req, contract.gateway_name)

            # Check if sending order successful
            if not vt_orderid:
                continue

            vt_orderids.append(vt_orderid)

            self.offset_converter.update_order_request(req, vt_orderid)

            # Save relationship between orderid and strategy.
            self.orderid_strategy_map[vt_orderid] = strategy
            self.strategy_orderid_map[strategy.strategy_name].add(vt_orderid)

        return vt_orderids

这是CtaEngine的send_server_order()函数,是把委托申请发送给交易服务器的函数,其中的这一句:
req.reference = strategy.strategy_name # Add strategy name as order reference
这是原来系统就有的一句,但是不知道为什么没有利用起来,现在我把它利用起来,它可以决定一个申请是手动的还是自动的。
手动的req.reference是空字符串,自动申请的req.reference是非空的策略名称!

2.2 委托申请变身为委托单

2.2.1 CtpGateway的TdApi的send_order()

    def send_order(self, req: OrderRequest):
        """
        Send new order.
        """
        if req.offset not in OFFSET_VT2CTP:
            self.gateway.write_log("请选择开平方向")
            return ""

        self.order_ref += 1

        ctp_req = {
            "InstrumentID": req.symbol,
            "ExchangeID": req.exchange.value,
            "LimitPrice": req.price,
            "VolumeTotalOriginal": int(req.volume),
            "OrderPriceType": ORDERTYPE_VT2CTP.get(req.type, ""),
            "Direction": DIRECTION_VT2CTP.get(req.direction, ""),
            "CombOffsetFlag": OFFSET_VT2CTP.get(req.offset, ""),
            "OrderRef": str(self.order_ref),
            "InvestorID": self.userid,
            "UserID": self.userid,
            "BrokerID": self.brokerid,
            "CombHedgeFlag": THOST_FTDC_HF_Speculation,
            "ContingentCondition": THOST_FTDC_CC_Immediately,
            "ForceCloseReason": THOST_FTDC_FCC_NotForceClose,
            "IsAutoSuspend": 0,
            "TimeCondition": THOST_FTDC_TC_GFD,
            "VolumeCondition": THOST_FTDC_VC_AV,
            "MinVolume": 1
        }

        if req.type == OrderType.FAK:
            ctp_req["OrderPriceType"] = THOST_FTDC_OPT_LimitPrice
            ctp_req["TimeCondition"] = THOST_FTDC_TC_IOC
            ctp_req["VolumeCondition"] = THOST_FTDC_VC_AV
        elif req.type == OrderType.FOK:
            ctp_req["OrderPriceType"] = THOST_FTDC_OPT_LimitPrice
            ctp_req["TimeCondition"] = THOST_FTDC_TC_IOC
            ctp_req["VolumeCondition"] = THOST_FTDC_VC_CV

        self.reqid += 1
        # print(f"!!!3 ctp_req={ctp_req}")  # hxxjava debug
        self.reqOrderInsert(ctp_req, self.reqid)

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

        return order.vt_orderid

这个函数是委托申请后最终执行的函数,这里的这一句非常重要:
order = req.create_order_data(orderid, self.gateway_name)
就是在create_order_data()里,完成了req.reference到order.reference的传递!

2.2.2 修改OrderData

@dataclass
class OrderData(BaseData):
    """
    Order data contains information for tracking lastest status
    of a specific order.
    """

    symbol: str
    exchange: Exchange
    orderid: str

    type: OrderType = OrderType.LIMIT
    direction: Direction = None
    offset: Offset = Offset.NONE
    price: float = 0
    volume: float = 0
    traded: float = 0
    status: Status = Status.SUBMITTING
    datetime: datetime = None
    reference:str = ""      # hxxjava add

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

    def is_active(self) -> bool:
        """
        Check if the order is active.
        """
        if self.status in ACTIVE_STATUSES:
            return True
        else:
            return False

    def create_cancel_request(self) -> "CancelRequest":
        """
        Create cancel request object from order.
        """
        req = CancelRequest(
            orderid=self.orderid, symbol=self.symbol, exchange=self.exchange
        )
        return req

2.2.3 修改OrderRequest的create_order_data()

@dataclass
class OrderRequest:
    """
    Request sending to specific gateway for creating a new order.
    """

    symbol: str
    exchange: Exchange
    direction: Direction
    type: OrderType
    volume: float
    price: float = 0
    offset: Offset = Offset.NONE
    reference: str = ""

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

    def create_order_data(self, orderid: str, gateway_name: str) -> OrderData:
        """
        Create order data from request.
        """
        order = OrderData(
            symbol=self.symbol,
            exchange=self.exchange,
            orderid=orderid,
            type=self.type,
            direction=self.direction,
            offset=self.offset,
            price=self.price,
            volume=self.volume,
            gateway_name=gateway_name,
            reference = self.reference      # hxxjava add
        )
        return order

2.3 委托单决定成交单的归属

OrderData.reference表示它属于哪个策略,或者是不是自动委托的
TradeData中通过orderid找到成交单属于哪个委托单的,进而可以间接确定它属于哪个策略

2.4 修改OrderMonitor,为其增加“策略”显示字段

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},
        "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": TimeCell, "update": True},
        "gateway_name": {"display": "接口", "cell": BaseCell, "update": False},
        "reference": {"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)

2.5 显示效果

description

1. 主要函数实现

from typing import List,Dict,Tuple,Optional,Sequence
from vnpy.trader.object import ContractData
from datetime import datetime,timedelta
import rqdatac as rq

def left_alphas(instr:str):
    """
    得到字符串左边的字符部分
    """
    ret_str = ''
    for s in instr:
        if s.isalpha():
            ret_str += s
        else:
            break
    return ret_str

def get_contract_kinds(contracts:List[ContractData]):
    """
    从合约列表中获取合约的品种和品种列表
    """
    kinds:Dict[str,List[ContractData]] = {}
    for contract in contracts:
        vt_symbol = contract.vt_symbol
        kind = left_alphas(vt_symbol)
        if kind not in kinds:
            kinds[kind] = [vt_symbol]
        else:
            if vt_symbol not in kinds[kind]:
                kinds[kind].append(vt_symbol)

    return kinds


def get_all_dominants(kinds:List[str], which_day:datetime=None):
    """ 主力合约排序  """
    if not which_day:
        which_day = datetime.today()-timedelta(days=3)

    dominants1 = []    
    for kind in kinds:    
        # 查询指定日期的合约品种的主力合约
        df = rq.futures.get_dominant(underlying_symbol=kind.upper(),start_date=which_day)
        vt_symbol = df.values[0]        
        # 查询主力合约在指定交易日的成交额度   
        df = rq.get_price(vt_symbol,frequency='1d',start_date=which_day,end_date=which_day)
        total_turnover = df['total_turnover'].values[0]

        dominants1.append({"kind":kind,"vt_symbol":vt_symbol,"total_turnover":total_turnover})

    # 按照成交额度降序排序  
    dominants = sorted(dominants1,key=lambda x:x["total_turnover"],reverse=True)
    return dominants

2. 找当前市场的所有正在交易的合约品种及主力合约

在vnpy\trader\ui\main_window.py中增加一个菜单,然后为其增加下面的处理函数:

    def query_contracts(self) -> None:  # hxxjava add
        """
        Query contracts test.
        """
        oms_engine:SamEngine = self.main_engine.get_engine('oms')
        contracts = oms_engine.contracts.values()
        # for row,contract in enumerate(contracts):
        #     print(f"【{row}】   {contract}")


        contract_kinds = get_contract_kinds(contracts)
        print("==============全市场品种及在交易合约列表========================")
        for kind,vt_symbols in contract_kinds.items():
            print(f"kind={kind} vt_symbols={vt_symbols}")

        kinds = [kind for kind in contract_kinds]
        dominants = get_all_dominants(kinds)
        print("=====================全市场主力合约排序========================")        
        for dominant in dominants:
            print(f"{dominant}")

3. 测试结果:

3.1 全市场品种及在交易合约列表

==============全市场品种及在交易合约列表========================
kind=CF vt_symbols=['CF109.CZCE', 'CF105.CZCE', 'CF107.CZCE', 'CF101.CZCE', 'CF103.CZCE', 'CF011.CZCE']
kind=TA vt_symbols=['TA109.CZCE', 'TA105.CZCE', 'TA104.CZCE', 'TA107.CZCE', 'TA101.CZCE', 'TA106.CZCE', 'TA108.CZCE', 'TA102.CZCE', 'TA103.CZCE', 'TA012.CZCE', 'TA011.CZCE', 'TA010.CZCE']
kind=SR vt_symbols=['SR109.CZCE', 'SR105.CZCE', 'SR107.CZCE', 'SR101.CZCE', 'SR103.CZCE', 'SR011.CZCE']
kind=CY vt_symbols=['CY109.CZCE', 'CY105.CZCE', 'CY104.CZCE', 'CY107.CZCE', 'CY101.CZCE', 'CY106.CZCE', 'CY108.CZCE', 'CY102.CZCE', 'CY103.CZCE', 'CY012.CZCE', 'CY011.CZCE', 'CY010.CZCE']
kind=FG vt_symbols=['FG109.CZCE', 'FG105.CZCE', 'FG104.CZCE', 'FG107.CZCE', 'FG101.CZCE', 'FG106.CZCE', 'FG108.CZCE', 'FG102.CZCE', 'FG103.CZCE', 'FG012.CZCE', 'FG011.CZCE', 'FG010.CZCE']
kind=sc vt_symbols=['sc2109.INE', 'sc2106.INE', 'sc2303.INE', 'sc2101.INE', 'sc2309.INE', 'sc2102.INE', 'sc2107.INE', 'sc2104.INE', 'sc2306.INE', 'sc2105.INE', 'sc2011.INE', 'sc2108.INE', 'sc2212.INE', 'sc2209.INE', 'sc2103.INE', 'sc2112.INE', 'sc2010.INE', 'sc2206.INE', 'sc2203.INE', 'sc2012.INE']
kind=ag vt_symbols=['ag2103.SHFE', 'ag2102.SHFE', 'ag2104.SHFE', 'ag2101.SHFE', 'ag2105.SHFE', 'ag2106.SHFE', 'ag2108.SHFE', 'ag2109.SHFE', 'ag2107.SHFE', 'ag2011.SHFE', 'ag2010.SHFE', 'ag2012.SHFE']
kind=al vt_symbols=['al2103.SHFE', 'al2102.SHFE', 'al2104.SHFE', 'al2101.SHFE', 'al2105.SHFE', 'al2106.SHFE', 'al2108.SHFE', 'al2109.SHFE', 'al2107.SHFE', 'al2011.SHFE', 'al2010.SHFE', 'al2012.SHFE']
kind=au vt_symbols=['au2104.SHFE', 'au2102.SHFE', 'au2106.SHFE', 'au2011.SHFE', 'au2110.SHFE', 'au2108.SHFE', 'au2012.SHFE', 'au2010.SHFE']
kind=bu vt_symbols=['bu2203.SHFE', 'bu2010.SHFE', 'bu2011.SHFE', 'bu2206.SHFE', 'bu2102.SHFE', 'bu2209.SHFE', 'bu2101.SHFE', 'bu2112.SHFE', 'bu2109.SHFE', 'bu2103.SHFE', 'bu2106.SHFE', 'bu2012.SHFE']
kind=cu vt_symbols=['cu2103.SHFE', 'cu2102.SHFE', 'cu2104.SHFE', 'cu2101.SHFE', 'cu2105.SHFE', 'cu2106.SHFE', 'cu2108.SHFE', 'cu2109.SHFE', 'cu2107.SHFE', 'cu2011.SHFE', 'cu2010.SHFE', 'cu2012.SHFE']
kind=hc vt_symbols=['hc2103.SHFE', 'hc2102.SHFE', 'hc2104.SHFE', 'hc2101.SHFE', 'hc2105.SHFE', 'hc2106.SHFE', 'hc2108.SHFE', 'hc2109.SHFE', 'hc2107.SHFE', 'hc2011.SHFE', 'hc2010.SHFE', 'hc2012.SHFE']
kind=ni vt_symbols=['ni2103.SHFE', 'ni2102.SHFE', 'ni2104.SHFE', 'ni2101.SHFE', 'ni2105.SHFE', 'ni2106.SHFE', 'ni2108.SHFE', 'ni2109.SHFE', 'ni2107.SHFE', 'ni2011.SHFE', 'ni2010.SHFE', 'ni2012.SHFE']
kind=pb vt_symbols=['pb2103.SHFE', 'pb2102.SHFE', 'pb2104.SHFE', 'pb2101.SHFE', 'pb2105.SHFE', 'pb2106.SHFE', 'pb2108.SHFE', 'pb2109.SHFE', 'pb2107.SHFE', 'pb2011.SHFE', 'pb2010.SHFE', 'pb2012.SHFE']
kind=rb vt_symbols=['rb2103.SHFE', 'rb2102.SHFE', 'rb2104.SHFE', 'rb2101.SHFE', 'rb2105.SHFE', 'rb2106.SHFE', 'rb2108.SHFE', 'rb2109.SHFE', 'rb2107.SHFE', 'rb2010.SHFE', 'rb2011.SHFE', 'rb2012.SHFE']
kind=ru vt_symbols=['ru2103.SHFE', 'ru2104.SHFE', 'ru2101.SHFE', 'ru2105.SHFE', 'ru2106.SHFE', 'ru2108.SHFE', 'ru2109.SHFE', 'ru2107.SHFE', 'ru2010.SHFE', 'ru2011.SHFE']
kind=sn vt_symbols=['sn2103.SHFE', 'sn2102.SHFE', 'sn2104.SHFE', 'sn2101.SHFE', 'sn2105.SHFE', 'sn2106.SHFE', 'sn2108.SHFE', 'sn2109.SHFE', 'sn2107.SHFE', 'sn2010.SHFE', 'sn2011.SHFE', 'sn2012.SHFE']
kind=sp vt_symbols=['sp2103.SHFE', 'sp2102.SHFE', 'sp2104.SHFE', 'sp2101.SHFE', 'sp2105.SHFE', 'sp2106.SHFE', 'sp2108.SHFE', 'sp2109.SHFE', 'sp2107.SHFE', 'sp2010.SHFE', 'sp2011.SHFE', 'sp2012.SHFE']
kind=wr vt_symbols=['wr2103.SHFE', 'wr2102.SHFE', 'wr2104.SHFE', 'wr2101.SHFE', 'wr2105.SHFE', 'wr2106.SHFE', 'wr2108.SHFE', 'wr2109.SHFE', 'wr2107.SHFE', 'wr2010.SHFE', 'wr2011.SHFE', 'wr2012.SHFE']
kind=zn vt_symbols=['zn2103.SHFE', 'zn2102.SHFE', 'zn2104.SHFE', 'zn2101.SHFE', 'zn2105.SHFE', 'zn2106.SHFE', 'zn2108.SHFE', 'zn2109.SHFE', 'zn2107.SHFE', 'zn2010.SHFE', 'zn2011.SHFE', 'zn2012.SHFE']
kind=PM vt_symbols=['PM103.CZCE', 'PM105.CZCE', 'PM101.CZCE', 'PM107.CZCE', 'PM109.CZCE', 'PM011.CZCE']
kind=RI vt_symbols=['RI103.CZCE', 'RI105.CZCE', 'RI101.CZCE', 'RI107.CZCE', 'RI109.CZCE', 'RI011.CZCE']
kind=RM vt_symbols=['RM103.CZCE', 'RM105.CZCE', 'RM101.CZCE', 'RM107.CZCE', 'RM108.CZCE', 'RM109.CZCE', 'RM011.CZCE']
kind=SF vt_symbols=['SF103.CZCE', 'SF105.CZCE', 'SF104.CZCE', 'SF101.CZCE', 'SF106.CZCE', 'SF107.CZCE', 'SF108.CZCE', 'SF109.CZCE', 'SF102.CZCE', 'SF012.CZCE', 'SF011.CZCE', 'SF010.CZCE']
kind=SM vt_symbols=['SM103.CZCE', 'SM105.CZCE', 'SM104.CZCE', 'SM101.CZCE', 'SM106.CZCE', 'SM107.CZCE', 'SM108.CZCE', 'SM109.CZCE', 'SM102.CZCE', 'SM012.CZCE', 'SM011.CZCE', 'SM010.CZCE']
kind=WH vt_symbols=['WH103.CZCE', 'WH105.CZCE', 'WH101.CZCE', 'WH107.CZCE', 'WH109.CZCE', 'WH011.CZCE']
kind=a vt_symbols=['a2103.DCE', 'a2105.DCE', 'a2107.DCE', 'a2101.DCE', 'a2109.DCE', 'a2011.DCE']
kind=b vt_symbols=['b2103.DCE', 'b2105.DCE', 'b2104.DCE', 'b2107.DCE', 'b2106.DCE', 'b2101.DCE', 'b2108.DCE', 'b2109.DCE', 'b2102.DCE', 'b2012.DCE', 'b2011.DCE', 'b2010.DCE']
kind=bb vt_symbols=['bb2103.DCE', 'bb2105.DCE', 'bb2104.DCE', 'bb2107.DCE', 'bb2106.DCE', 'bb2101.DCE', 'bb2108.DCE', 'bb2109.DCE', 'bb2102.DCE', 'bb2012.DCE', 'bb2011.DCE', 'bb2010.DCE']
kind=c vt_symbols=['c2103.DCE', 'c2105.DCE', 'c2107.DCE', 'c2101.DCE', 'c2109.DCE', 'c2011.DCE']
kind=cs vt_symbols=['cs2103.DCE', 'cs2105.DCE', 'cs2107.DCE', 'cs2101.DCE', 'cs2109.DCE', 'cs2011.DCE']
kind=fb vt_symbols=['fb2103.DCE', 'fb2105.DCE', 'fb2104.DCE', 'fb2107.DCE', 'fb2106.DCE', 'fb2101.DCE', 'fb2108.DCE', 'fb2109.DCE', 'fb2102.DCE', 'fb2012.DCE', 'fb2011.DCE', 'fb2010.DCE']
kind=i vt_symbols=['i2103.DCE', 'i2105.DCE', 'i2104.DCE', 'i2107.DCE', 'i2106.DCE', 'i2101.DCE', 'i2108.DCE', 'i2010.DCE', 'i2109.DCE', 'i2102.DCE', 'i2012.DCE', 'i2011.DCE']
kind=j vt_symbols=['j2103.DCE', 'j2105.DCE', 'j2104.DCE', 'j2107.DCE', 'j2106.DCE', 'j2101.DCE', 'j2108.DCE', 'j2010.DCE', 'j2109.DCE', 'j2102.DCE', 'j2012.DCE', 'j2011.DCE']
kind=jm vt_symbols=['jm2103.DCE', 'jm2105.DCE', 'jm2104.DCE', 'jm2107.DCE', 'jm2106.DCE', 'jm2101.DCE', 'jm2108.DCE', 'jm2010.DCE', 'jm2109.DCE', 'jm2102.DCE', 'jm2012.DCE', 'jm2011.DCE']
kind=l vt_symbols=['l2103.DCE', 'l2105.DCE', 'l2104.DCE', 'l2107.DCE', 'l2101.DCE', 'l2106.DCE', 'l2108.DCE', 'l2010.DCE', 'l2109.DCE', 'l2102.DCE', 'l2012.DCE', 'l2011.DCE']
kind=m vt_symbols=['m2103.DCE', 'm2105.DCE', 'm2107.DCE', 'm2101.DCE', 'm2108.DCE', 'm2109.DCE', 'm2012.DCE', 'm2011.DCE']
kind=p vt_symbols=['p2103.DCE', 'p2105.DCE', 'p2104.DCE', 'p2107.DCE', 'p2101.DCE', 'p2106.DCE', 'p2108.DCE', 'p2010.DCE', 'p2109.DCE', 'p2102.DCE', 'p2012.DCE', 'p2011.DCE']
kind=pp vt_symbols=['pp2103.DCE', 'pp2105.DCE', 'pp2104.DCE', 'pp2107.DCE', 'pp2101.DCE', 'pp2106.DCE', 'pp2108.DCE', 'pp2010.DCE', 'pp2109.DCE', 'pp2102.DCE', 'pp2012.DCE', 'pp2011.DCE']
kind=v vt_symbols=['v2103.DCE', 'v2105.DCE', 'v2109.DCE', 'v2104.DCE', 'v2107.DCE', 'v2101.DCE', 'v2106.DCE', 'v2108.DCE', 'v2010.DCE', 'v2102.DCE', 'v2012.DCE', 'v2011.DCE']
kind=y vt_symbols=['y2103.DCE', 'y2105.DCE', 'y2109.DCE', 'y2107.DCE', 'y2101.DCE', 'y2108.DCE', 'y2012.DCE', 'y2011.DCE']
kind=IC vt_symbols=['IC2012.CFFEX', 'IC2103.CFFEX', 'IC2010.CFFEX', 'IC2011.CFFEX']
kind=IF vt_symbols=['IF2012.CFFEX', 'IF2103.CFFEX', 'IF2010.CFFEX', 'IF2011.CFFEX']
kind=IH vt_symbols=['IH2012.CFFEX', 'IH2103.CFFEX', 'IH2010.CFFEX', 'IH2011.CFFEX']
kind=ZC vt_symbols=['ZC105.CZCE', 'ZC109.CZCE', 'ZC106.CZCE', 'ZC104.CZCE', 'ZC102.CZCE', 'ZC103.CZCE', 'ZC107.CZCE', 'ZC108.CZCE', 'ZC101.CZCE', 'ZC011.CZCE', 'ZC010.CZCE', 'ZC012.CZCE']
kind=eg vt_symbols=['eg2106.DCE', 'eg2105.DCE', 'eg2101.DCE', 'eg2102.DCE', 'eg2104.DCE', 'eg2010.DCE', 'eg2103.DCE', 'eg2107.DCE', 'eg2108.DCE', 'eg2011.DCE', 'eg2012.DCE', 'eg2109.DCE']
kind=jd vt_symbols=['jd2106.DCE', 'jd2105.DCE', 'jd2101.DCE', 'jd2102.DCE', 'jd2104.DCE', 'jd2010.DCE', 'jd2103.DCE', 'jd2107.DCE', 'jd2108.DCE', 'jd2011.DCE', 'jd2012.DCE', 'jd2109.DCE']
kind=fu vt_symbols=['fu2103.SHFE', 'fu2101.SHFE', 'fu2109.SHFE', 'fu2102.SHFE', 'fu2107.SHFE', 'fu2104.SHFE', 'fu2106.SHFE', 'fu2105.SHFE', 'fu2011.SHFE', 'fu2108.SHFE', 'fu2012.SHFE', 'fu2010.SHFE']
kind=AP vt_symbols=['AP105.CZCE', 'AP107.CZCE', 'AP101.CZCE', 'AP103.CZCE', 'AP012.CZCE', 'AP011.CZCE', 'AP010.CZCE']
kind=JR vt_symbols=['JR105.CZCE', 'JR107.CZCE', 'JR101.CZCE', 'JR109.CZCE', 'JR103.CZCE', 'JR011.CZCE']
kind=LR vt_symbols=['LR105.CZCE', 'LR011.CZCE', 'LR101.CZCE', 'LR107.CZCE', 'LR109.CZCE', 'LR103.CZCE']
kind=MA vt_symbols=['MA105.CZCE', 'MA104.CZCE', 'MA101.CZCE', 'MA106.CZCE', 'MA107.CZCE', 'MA108.CZCE', 'MA109.CZCE', 'MA102.CZCE', 'MA103.CZCE', 'MA012.CZCE', 'MA011.CZCE', 'MA010.CZCE']
kind=OI vt_symbols=['OI105.CZCE', 'OI101.CZCE', 'OI107.CZCE', 'OI109.CZCE', 'OI103.CZCE', 'OI011.CZCE']
kind=eb vt_symbols=['eb2010.DCE', 'eb2011.DCE', 'eb2012.DCE', 'eb2101.DCE', 'eb2102.DCE', 'eb2103.DCE', 'eb2104.DCE', 'eb2105.DCE', 'eb2106.DCE', 'eb2107.DCE', 'eb2108.DCE', 'eb2109.DCE']
kind=rr vt_symbols=['rr2010.DCE', 'rr2011.DCE', 'rr2012.DCE', 'rr2101.DCE', 'rr2102.DCE', 'rr2103.DCE', 'rr2104.DCE', 'rr2105.DCE', 'rr2106.DCE', 'rr2107.DCE', 'rr2108.DCE', 'rr2109.DCE']
kind=pg vt_symbols=['pg2011.DCE', 'pg2012.DCE', 'pg2101.DCE', 'pg2102.DCE', 'pg2103.DCE', 'pg2104.DCE', 'pg2105.DCE', 'pg2106.DCE', 'pg2107.DCE', 'pg2108.DCE', 'pg2109.DCE']
kind=T vt_symbols=['T2103.CFFEX', 'T2106.CFFEX', 'T2012.CFFEX']
kind=TF vt_symbols=['TF2103.CFFEX', 'TF2106.CFFEX', 'TF2012.CFFEX']
kind=TS vt_symbols=['TS2103.CFFEX', 'TS2106.CFFEX', 'TS2012.CFFEX']
kind=RS vt_symbols=['RS107.CZCE', 'RS108.CZCE', 'RS109.CZCE', 'RS011.CZCE']
kind=UR vt_symbols=['UR107.CZCE', 'UR108.CZCE', 'UR109.CZCE', 'UR010.CZCE', 'UR011.CZCE', 'UR012.CZCE', 'UR101.CZCE', 'UR102.CZCE', 'UR103.CZCE', 'UR104.CZCE', 'UR105.CZCE', 'UR106.CZCE']

3.2 全市场主力合约排序

=====================全市场主力合约排序========================
{'kind': 'ag', 'vt_symbol': 'AG2012', 'total_turnover': 214143898665.0}
{'kind': 'IF', 'vt_symbol': 'IF2010', 'total_turnover': 115089726720.0}
{'kind': 'IC', 'vt_symbol': 'IC2010', 'total_turnover': 108709168960.0}
{'kind': 'au', 'vt_symbol': 'AU2012', 'total_turnover': 97275079200.0}
{'kind': 'i', 'vt_symbol': 'I2101', 'total_turnover': 70141287600.0}
{'kind': 'T', 'vt_symbol': 'T2012', 'total_turnover': 66779643250.0}
{'kind': 'ru', 'vt_symbol': 'RU2101', 'total_turnover': 62175496100.0}
{'kind': 'y', 'vt_symbol': 'Y2101', 'total_turnover': 62036693940.0}
{'kind': 'p', 'vt_symbol': 'P2101', 'total_turnover': 60139173280.0}
{'kind': 'OI', 'vt_symbol': 'OI2101', 'total_turnover': 58055810880.0}
{'kind': 'cu', 'vt_symbol': 'CU2011', 'total_turnover': 53450265650.0}
{'kind': 'FG', 'vt_symbol': 'FG2101', 'total_turnover': 45607836000.0}
{'kind': 'm', 'vt_symbol': 'M2101', 'total_turnover': 35834947080.0}
{'kind': 'rb', 'vt_symbol': 'RB2101', 'total_turnover': 35004834540.0}
{'kind': 'j', 'vt_symbol': 'J2101', 'total_turnover': 32930215200.0}
{'kind': 'IH', 'vt_symbol': 'IH2010', 'total_turnover': 28645460100.0}
{'kind': 'sc', 'vt_symbol': 'SC2011', 'total_turnover': 28389715200.0}
{'kind': 'zn', 'vt_symbol': 'ZN2011', 'total_turnover': 25126207275.0}
{'kind': 'fu', 'vt_symbol': 'FU2101', 'total_turnover': 24160222910.0}
{'kind': 'ZC', 'vt_symbol': 'ZC2011', 'total_turnover': 21100200000.0}
{'kind': 'pp', 'vt_symbol': 'PP2101', 'total_turnover': 20908030425.0}
{'kind': 'AP', 'vt_symbol': 'AP2101', 'total_turnover': 20720715330.0}
{'kind': 'bu', 'vt_symbol': 'BU2012', 'total_turnover': 20684842140.0}
{'kind': 'TF', 'vt_symbol': 'TF2012', 'total_turnover': 20387290900.0}
{'kind': 'ni', 'vt_symbol': 'NI2011', 'total_turnover': 18114747280.0}
{'kind': 'SR', 'vt_symbol': 'SR2101', 'total_turnover': 17356500420.0}
{'kind': 'MA', 'vt_symbol': 'MA2101', 'total_turnover': 17291912680.0}
{'kind': 'CF', 'vt_symbol': 'CF2101', 'total_turnover': 14776293750.0}
{'kind': 'jd', 'vt_symbol': 'JD2011', 'total_turnover': 14047527750.0}
{'kind': 'c', 'vt_symbol': 'C2101', 'total_turnover': 13462882980.0}
{'kind': 'RM', 'vt_symbol': 'RM2101', 'total_turnover': 12374485580.0}
{'kind': 'al', 'vt_symbol': 'AL2011', 'total_turnover': 12072076375.0}
{'kind': 'l', 'vt_symbol': 'L2101', 'total_turnover': 11536233225.0}
{'kind': 'TS', 'vt_symbol': 'TS2012', 'total_turnover': 9449540000.0}
{'kind': 'TA', 'vt_symbol': 'TA2101', 'total_turnover': 9398714690.0}
{'kind': 'eg', 'vt_symbol': 'EG2101', 'total_turnover': 8941426960.0}
{'kind': 'pg', 'vt_symbol': 'PG2011', 'total_turnover': 8926672280.0}
{'kind': 'sp', 'vt_symbol': 'SP2012', 'total_turnover': 7528736540.0}
{'kind': 'v', 'vt_symbol': 'V2101', 'total_turnover': 7187423175.0}
{'kind': 'a', 'vt_symbol': 'A2101', 'total_turnover': 6813036460.0}
{'kind': 'hc', 'vt_symbol': 'HC2101', 'total_turnover': 6807265320.0}
{'kind': 'jm', 'vt_symbol': 'JM2101', 'total_turnover': 4624123680.0}
{'kind': 'sn', 'vt_symbol': 'SN2012', 'total_turnover': 4067929330.0}
{'kind': 'pb', 'vt_symbol': 'PB2011', 'total_turnover': 3304319225.0}
{'kind': 'SM', 'vt_symbol': 'SM2101', 'total_turnover': 2529065430.0}
{'kind': 'cs', 'vt_symbol': 'CS2101', 'total_turnover': 2402811690.0}
{'kind': 'b', 'vt_symbol': 'B2011', 'total_turnover': 2338601160.0}
{'kind': 'eb', 'vt_symbol': 'EB2101', 'total_turnover': 1968052130.0}
{'kind': 'UR', 'vt_symbol': 'UR2101', 'total_turnover': 1476473040.0}
{'kind': 'CY', 'vt_symbol': 'CY2101', 'total_turnover': 707752500.0}
{'kind': 'SF', 'vt_symbol': 'SF2101', 'total_turnover': 507498280.0}
{'kind': 'rr', 'vt_symbol': 'RR2012', 'total_turnover': 505222820.0}
{'kind': 'fb', 'vt_symbol': 'FB2010', 'total_turnover': 38542900.0}
{'kind': 'WH', 'vt_symbol': 'WH2101', 'total_turnover': 1927700.0}
{'kind': 'LR', 'vt_symbol': 'LR2011', 'total_turnover': 346200.0}
{'kind': 'RS', 'vt_symbol': 'RS2011', 'total_turnover': 264500.0}
{'kind': 'wr', 'vt_symbol': 'WR2103', 'total_turnover': 0.0}
{'kind': 'PM', 'vt_symbol': 'PM2105', 'total_turnover': 0.0}
{'kind': 'RI', 'vt_symbol': 'RI2101', 'total_turnover': 0.0}
{'kind': 'bb', 'vt_symbol': 'BB2107', 'total_turnover': 0.0}
{'kind': 'JR', 'vt_symbol': 'JR2101', 'total_turnover': 0.0}

1. 让人蒙圈的python类型注解

database_manager:"BaseDatabaseManager"是个什么东东?
这个东西是python 3.6之后增加的新特性,看看这篇文章就知道了:Python 3 新特性:类型注解

本来应该是:
database_manager:BaseDatabaseManager,但是代码的作者可能是遇到了python包包含问题,
所以改成了:
database_manager:"BaseDatabaseManager",其实还是一个类型注解,只对类编辑环境有意义,对执行没有影响。

2. 看看下面的简单例子就可以明白

就比如:

>>> a : int = 10
>>> b : int = 1.2
>>> c:"int" = 3
>>> d:"int" = 3.1
>>> type(a)
<class 'int'>
>>> type(b)
<class 'float'>
>>> type(c)
<class 'int'>
>>> type(d)
<class 'float'>

无论a,b,c,d的类型注解为什么,随着它们的赋值不同,它们变量的类型是不一样的。

3. 结论:

变量类型注解只是给代码编辑器使用和方便读者理解用的,不会限制实际赋值之后的数据类型。

OmsEngine从消息引擎订阅持仓消息

1. OmsEngine.register_event()注册EVENT_POSITION的处理句柄

    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)

2. 连接网关后得到交易服务器的持仓推送

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

        print(f"oms process_position_event ! {datetime.now()}")

3. 每4秒推送一次持仓,可以看到:

运行方式:python -m vnstation

oms process_position_event ! 2020-09-25 08:22:09.572302
oms process_position_event ! 2020-09-25 08:22:13.575021
oms process_position_event ! 2020-09-25 08:22:17.572799
oms process_position_event ! 2020-09-25 08:22:21.576441
oms process_position_event ! 2020-09-25 08:22:25.580222
  ...
  ...

1. 典型的绘图部件

保存文件:vnpy\usertools\chart_items.py
其中包含:

  • LineItem
  • RsiItem
  • SmaItem
  • MacdItem
  • TradeItem
  • OrderItem
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

from collections import OrderedDict
import pytz
CHINA_TZ = pytz.timezone("Asia/Shanghai")


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.blue_pen: QtGui.QPen = pg.mkPen(color=(100, 100, 255), width=2)

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

    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.blue_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 {sma_value:.1f}"
        else:
            text = "SMA -"

        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))

""" 将y方向的显示范围扩大到1.1 """
def adjust_range(in_range:Tuple[float, float])->Tuple[float, float]:
    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:
            return 0.0, 1.0

        # 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:
        # """"""
        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}"
                ]
            text = "\n".join(words)
        else:
            text = "diff - \ndea - \nmacd -"

        return text


class TradeItem(ScatterPlotItem,CandleItem): 
    """
    成交单绘图部件
    """
    def __init__(self, manager: BarManager):
        """"""
        ScatterPlotItem.__init__(self)
        # CandleItem.__init__(self,manager)
        # super(TradeItem,self).__init__(manager)
        super(CandleItem,self).__init__(manager)


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

        self.trades : Dict[int,Dict[str,TradeData]] = {} # {ix:{tradeid:trade}}

    def add_trades(self,trades:List[TradeData]):
        """ 增加成交单列表到TradeItem """
        for trade in trades:
            self.add_trade(trade)

        self.set_scatter_data()
        self.update()

    def add_trade(self,trade:TradeData,draw:bool=False):
        """ 增加一个成交单到TradeItem """
        # 这里使用reverse=True,是考虑到实盘成交往往发生在最新的bar里,可以加快搜索速度
        od = OrderedDict(sorted(self._manager._datetime_index_map.items(),key = lambda t:t[0],reverse=True))
        idx = self._manager.get_count() - 1
        for dt,ix in od.items():
            # print(f"dt={dt}\ntrade.datetime {trade.datetime}")
            dt1 = CHINA_TZ.localize(datetime.combine(dt.date(),dt.time()))
            if dt1 <= trade.datetime:
                # print(f"【dt={dt},dt1={dt1},dt2={trade.datetime} ix={ix}】")
                idx = ix
                break

        # 注意:一个bar期间可能发生多个成交单
        if idx in self.trades:
            self.trades[idx][trade.tradeid] = trade
        else:
            self.trades[idx] = {trade.tradeid:trade}

        if draw:        
            self.set_scatter_data()
            self.update()

        # print(f"add_trade idx={idx} trade={trade}")

    def set_scatter_data(self):
        """ 把成交单列表绘制到ScatterPlotItem上 """
        scatter_datas = []
        for ix in self.trades:
            for trade in self.trades[ix].values():
                scatter = {
                    "pos" : (ix, trade.price),
                    "data": 1,
                    "size": 14,
                    "pen": pg.mkPen((255, 255, 255)),
                }

                if trade.direction == Direction.LONG:
                    scatter_symbol = "t1"   # Up arrow
                else:
                    scatter_symbol = "t"    # Down arrow

                if trade.offset == Offset.OPEN:
                    scatter_brush = pg.mkBrush((255, 255, 0))   # Yellow
                else:
                    scatter_brush = pg.mkBrush((0, 0, 255))     # Blue

                scatter["symbol"] = scatter_symbol
                scatter["brush"] = scatter_brush
                scatter_datas.append(scatter)

        self.setData(scatter_datas)

    def get_info_text(self, ix: int) -> str:
        """"""
        if ix in self.trades:
            text = "成交:"
            for tradeid,trade in self.trades[ix].items():
                # TradeData
                text += f"\n{trade.price}{trade.direction.value}{trade.offset.value}{trade.volume}手"
        else:
            text = "成交:-"

        return text


class OrderItem(ScatterPlotItem,CandleItem): 
    """
    委托单绘图部件
    """
    def __init__(self, manager: BarManager):
        """"""
        ScatterPlotItem.__init__(self)
        super(CandleItem,self).__init__(manager)

        self.orders : Dict[int,Dict[str,Order]] = {} # {ix:{orderid:order}}

    def add_orders(self,orders:List[OrderData]):
        """ 增加委托单列表到OrderItem """
        for order in orders:
            if order.datetime:
                self.add_order(order)

        self.set_scatter_data()
        self.update()

    def add_order(self,order:OrderData,draw:bool=False):
        """ 增加一个委托单到OrderItem """
        # 这里使用reverse=True,是考虑到实盘成交往往发生在最新的bar里,可以加快搜索速度

        od = OrderedDict(sorted(self._manager._datetime_index_map.items(),key = lambda t:t[0],reverse=True))
        idx = self._manager.get_count() - 1
        for dt,ix in od.items():
            # print(f"dt={dt}\ntrade.datetime {trade.datetime}")
            dt1 = CHINA_TZ.localize(datetime.combine(dt.date(),dt.time()))
            if dt1 <= order.datetime:
                # print(f"【dt={dt},dt1={dt1},dt2={order.datetime} ix={ix}】")
                idx = ix
                break

        # 注意:一个bar期间可能发生多个委托单
        if idx in self.orders:
            self.orders[idx][order.orderid] = order
        else:
            self.orders[idx] = {order.orderid:order}

        if draw:
            self.set_scatter_data()
            self.update()

    def set_scatter_data(self):
        """ 把委托单列表绘制到ScatterPlotItem上 """
        scatter_datas = []
        for ix in self.orders:
            lowest,highest=self.get_y_range()
            # print(f"range={lowest,highest}")
            for order in self.orders[ix].values():
                # 处理委托报价超出显示范围的问题
                if order.price>highest:
                    show_price = highest - 7
                elif order.price<lowest:
                    show_price = lowest + 7
                else:
                    show_price = order.price 

                scatter = {
                    "pos" : (ix, show_price),
                    "data": 1,
                    "size": 14,
                    "pen": pg.mkPen((255, 255, 255)),
                }

                if order.direction == Direction.LONG:
                    scatter_symbol = "t1"   # Up arrow
                else:
                    scatter_symbol = "t"    # Down arrow

                if order.offset == Offset.OPEN:
                    scatter_brush = pg.mkBrush((0, 128, 128))   # Yellow
                else:
                    scatter_brush = pg.mkBrush((128, 128, 0))     # Blue

                scatter["symbol"] = scatter_symbol
                scatter["brush"] = scatter_brush
                scatter_datas.append(scatter)

        self.setData(scatter_datas)

    def get_info_text(self, ix: int) -> str:
        """"""
        if ix in self.orders:
            text = "委托:"
            for orderid,order in self.orders[ix].items():
                # OrderData
                text += f"\n{order.price}{order.direction.value}{order.offset.value}{order.volume}手"
        else:
            text = "委托:-"


        return text

2. 修改vnpy\chart\widget.py

为ChartWidget类增加下面的函数:

    def get_item(self,item_name:str):   # hxxjava add
        """
        Get chart item by item's name.
        """
        return self._items.get(item_name,None)

3. K线图表——各绘图部件使用方法演示

保存文件:vnpy\usertools\kx_chart.py

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

import numpy as np
import pyqtgraph as pg
import talib
import copy

from vnpy.trader.ui import create_qapp, QtCore, QtGui, QtWidgets
from vnpy.trader.database import database_manager
from vnpy.trader.constant import Exchange, Interval
from vnpy.trader.object import BarData

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

from vnpy.trader.engine import MainEngine
from vnpy.event import Event, EventEngine

from vnpy.trader.event import (
    EVENT_TICK,
    EVENT_TRADE,
    EVENT_ORDER,
    EVENT_POSITION,
    EVENT_ACCOUNT,
    EVENT_LOG
)

from vnpy.app.cta_strategy.base import (   
    EVENT_CTA_TICK,     
    EVENT_CTA_BAR,      
    EVENT_CTA_ORDER,    
    EVENT_CTA_TRADE,     
    EVENT_CTA_HISTORY_BAR
)

from vnpy.trader.object import (
    Direction, 
    Exchange, 
    Interval, 
    Offset, 
    Status, 
    Product, 
    OptionType, 
    OrderType,
    OrderData,
    TradeData,
)

from vnpy.usertools.chart_items import (
    LineItem,
    RsiItem,
    SmaItem,
    MacdItem,
    TradeItem,
    OrderItem,
)


class NewChartWidget(ChartWidget):
    """ 
    基于ChartWidget的K线图表 
    """
    MIN_BAR_COUNT = 100

    signal_cta_history_bar:QtCore.pyqtSignal = QtCore.pyqtSignal(Event)
    signal_cta_tick: QtCore.pyqtSignal = QtCore.pyqtSignal(Event)
    signal_cta_bar:QtCore.pyqtSignal = QtCore.pyqtSignal(Event)

    def __init__(self, parent: QtWidgets.QWidget = None,event_engine: EventEngine = None,strategy_name:str=""):
        """ 初始化 """
        super().__init__(parent)
        self.strategy_name = strategy_name
        self.event_engine = event_engine

        # 创建K线主图及多个绘图部件
        self.add_plot("candle", hide_x_axis=True)
        self.add_item(CandleItem, "candle", "candle")
        self.add_item(LineItem, "line", "candle")
        self.add_item(SmaItem, "sma", "candle")
        self.add_item(OrderItem, "order", "candle")
        self.add_item(TradeItem, "trade", "candle")

        # 创建成交量附图及绘图部件
        self.add_plot("volume", maximum_height=150)
        self.add_item(VolumeItem, "volume", "volume")

        # 创建RSI附图及绘图部件
        self.add_plot("rsi", maximum_height=150)
        self.add_item(RsiItem, "rsi", "rsi")

        # 创建MACD附图及绘图部件
        self.add_plot("macd", maximum_height=150)
        self.add_item(MacdItem, "macd", "macd")

        # 创建最新价格线、光标
        self.add_last_price_line()
        self.add_cursor()
        self.setWindowTitle(f"K线图表——{symbol}.{exchange.value},{interval},{start}-{end}")

        # 委托单列表
        self.orders:List[str,OrderData] = {}
        # 成交单列表
        self.trades:List[str,TradeData] = {}

        # self.register_event()
        # self.event_engine.start()

    def register_event(self) -> None:
        """"""
        self.signal_cta_history_bar.connect(self.process_cta_history_bar)
        self.event_engine.register(EVENT_CTA_HISTORY_BAR, self.signal_cta_history_bar.emit)

        self.signal_cta_tick.connect(self.process_tick_event)
        self.event_engine.register(EVENT_CTA_TICK, self.signal_cta_tick.emit)

        self.signal_cta_bar.connect(self.process_cta_bar)
        self.event_engine.register(EVENT_CTA_BAR, self.signal_cta_bar.emit)

    def process_cta_history_bar(self, event:Event) -> None:
        """ 处理历史K线推送 """
        strategy_name,history_bars = event.data
        if strategy_name == self.strategy_name:
            self.update_history(history_bars)

            # print(f" {strategy_name} got an EVENT_CTA_HISTORY_BAR")

    def process_tick_event(self, event: Event) -> None:
        """ 处理tick数据推送 """
        strategy_name,tick = event.data
        if strategy_name == self.strategy_name:
            if self.last_price_line:
                self.last_price_line.setValue(tick.last_price)
            #print(f" {strategy_name} got an EVENT_CTA_TICK")

    def process_cta_bar(self, event:Event)-> None:
        """ 处理K线数据推送 """

        strategy_name,bar = event.data
        if strategy_name == self.strategy_name:
            self.update_bar(bar)
            # print(f"{strategy_name} got an EVENT_CTA_BAR")

    def add_last_price_line(self):
        """"""
        plot = list(self._plots.values())[0]
        color = (255, 255, 255)

        self.last_price_line = pg.InfiniteLine(
            angle=0,
            movable=False,
            label="{value:.1f}",
            pen=pg.mkPen(color, width=1),
            labelOpts={
                "color": color,
                "position": 1,
                "anchors": [(1, 1), (1, 1)]
            }
        )
        self.last_price_line.label.setFont(NORMAL_FONT)
        plot.addItem(self.last_price_line)

    def update_history(self, history: List[BarData]) -> None:
        """
        Update a list of bar data.
        """
        self._manager.update_history(history)

        for item in self._items.values():
            item.update_history(history)

        self._update_plot_limits()

        self.move_to_right()

        self.update_last_price_line(history[-1])

    def update_bar(self, bar: BarData) -> None:
        """
        Update single bar data.
        """
        self._manager.update_bar(bar)

        for item in self._items.values():
            item.update_bar(bar)

        self._update_plot_limits()

        if self._right_ix >= (self._manager.get_count() - self._bar_count / 2):
            self.move_to_right()

        self.update_last_price_line(bar)

    def update_last_price_line(self, bar: BarData) -> None:
        """"""
        if self.last_price_line:
            self.last_price_line.setValue(bar.close_price)

    def add_orders(self,orders:List[OrderData]) -> None:
        """ 
        增加委托单列表到委托单绘图部件 
        """
        for order in orders:
            self.orders[order.orderid] = order

        order_item : OrderItem = self.get_item('order')
        if order_item:
            order_item.add_orders(self.orders.values())

    def add_trades(self,trades:List[TradeData]) -> None:
        """ 
        增加成交单列表到委托单绘图部件 
        """
        for trade in trades:
            self.trades[trade.tradeid] = trade

        trade_item : TradeItem = self.get_item('trade')
        if trade_item:
            trade_item.add_trades(self.trades.values())



################################################################
# 以下为测试代码
if __name__ == "__main__":
    def make_trades():
        import pytz
        CHINA_TZ = pytz.timezone("Asia/Shanghai")
        from vnpy.trader.object import Direction, Exchange, Interval, Offset, Status, Product, OptionType, OrderType,TradeData
        trades = [
            TradeData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_753490688_1', tradeid='         455', direction=Direction.LONG, offset=Offset.OPEN, price=6131.0, volume=3, datetime=CHINA_TZ.localize(datetime(2020, 8, 13, 21, 0, 1))),
            TradeData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_753490688_2', tradeid='       12738', direction=Direction.LONG, offset=Offset.OPEN, price=6142.0, volume=3, datetime=CHINA_TZ.localize(datetime(2020, 8, 13, 21, 14, 46))),
            TradeData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_753490688_3', tradeid='       16233', direction=Direction.LONG, offset=Offset.OPEN, price=6158.0, volume=3, datetime=CHINA_TZ.localize(datetime(2020, 8, 13, 21, 21, 59))),
            TradeData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_753490688_4', tradeid='       22815', direction=Direction.LONG, offset=Offset.OPEN, price=6180.0, volume=3, datetime=CHINA_TZ.localize(datetime(2020, 8, 13, 21, 39, 53))),
            TradeData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_1962356227_1', tradeid='       67570', direction=Direction.SHORT, offset=Offset.CLOSEYESTERDAY, price=6400.0, volume=12, datetime=CHINA_TZ.localize(datetime(2020, 8, 14, 1, 44,35))),
        ]
        return trades

    def make_orders():
        import pytz
        CHINA_TZ = pytz.timezone("Asia/Shanghai")

        orders = [
            OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_753490688_1', type=OrderType.LIMIT, direction=Direction.LONG, offset=Offset.OPEN, price=6494.0, volume=3.0, traded=0, status=Status.SUBMITTING, datetime=None, reference='TTS-ag2012'),
            OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_753490688_1', type=OrderType.LIMIT, direction=Direction.LONG, offset=Offset.OPEN, price=6494.0, volume=3, traded=0, status=Status.SUBMITTING, datetime=CHINA_TZ.localize(datetime(2020, 8, 13, 21, 0, 1)), reference=''),
            OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_753490688_1', type=OrderType.LIMIT, direction=Direction.LONG, offset=Offset.OPEN, price=6494.0, volume=3, traded=0, status=Status.SUBMITTING, datetime=CHINA_TZ.localize(datetime(2020, 8, 13, 21, 0, 1)), reference=''),
            OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_753490688_1', type=OrderType.LIMIT, direction=Direction.LONG, offset=Offset.OPEN, price=6494.0, volume=3, traded=3, status=Status.ALLTRADED, datetime=CHINA_TZ.localize(datetime(2020, 8, 13, 21, 0, 1)), reference=''),

            OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_753490688_2', type=OrderType.LIMIT, direction=Direction.LONG, offset=Offset.OPEN, price=6494.0, volume=3.0, traded=0, status=Status.SUBMITTING, datetime=None, reference='TTS-ag2012'),
            OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_753490688_2', type=OrderType.LIMIT, direction=Direction.LONG, offset=Offset.OPEN, price=6494.0, volume=3, traded=0, status=Status.SUBMITTING, datetime=CHINA_TZ.localize(datetime(2020, 8, 13, 21, 14, 46)), reference=''),
            OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_753490688_2', type=OrderType.LIMIT, direction=Direction.LONG, offset=Offset.OPEN, price=6494.0, volume=3, traded=0, status=Status.SUBMITTING, datetime=CHINA_TZ.localize(datetime(2020, 8, 13, 21, 14, 46)), reference=''),
            OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_753490688_2', type=OrderType.LIMIT, direction=Direction.LONG, offset=Offset.OPEN, price=6494.0, volume=3, traded=3, status=Status.ALLTRADED, datetime=CHINA_TZ.localize(datetime(2020, 8, 13, 21, 14, 46)), reference=''),

            OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_753490688_3', type=OrderType.LIMIT, direction=Direction.LONG, offset=Offset.OPEN, price=6494.0, volume=3.0, traded=0, status=Status.SUBMITTING, datetime=None, reference='TTS-ag2012'),
            OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_753490688_3', type=OrderType.LIMIT, direction=Direction.LONG, offset=Offset.OPEN, price=6494.0, volume=3, traded=0, status=Status.SUBMITTING, datetime=CHINA_TZ.localize(datetime(2020, 8, 13, 21, 21, 59)), reference=''),
            OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_753490688_3', type=OrderType.LIMIT, direction=Direction.LONG, offset=Offset.OPEN, price=6494.0, volume=3, traded=0, status=Status.SUBMITTING, datetime=CHINA_TZ.localize(datetime(2020, 8, 13, 21, 21, 59)), reference=''),
            OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_753490688_3', type=OrderType.LIMIT, direction=Direction.LONG, offset=Offset.OPEN, price=6494.0, volume=3, traded=3, status=Status.ALLTRADED, datetime=CHINA_TZ.localize(datetime(2020, 8, 13, 21, 21, 59)), reference=''),

            OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_753490688_4', type=OrderType.LIMIT, direction=Direction.LONG, offset=Offset.OPEN, price=6494.0, volume=3.0, traded=0, status=Status.SUBMITTING, datetime=None, reference='TTS-ag2012'),
            OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_753490688_4', type=OrderType.LIMIT, direction=Direction.LONG, offset=Offset.OPEN, price=6494.0, volume=3, traded=0, status=Status.SUBMITTING, datetime=CHINA_TZ.localize(datetime(2020, 8, 13, 21, 39, 53)), reference=''),
            OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_753490688_4', type=OrderType.LIMIT, direction=Direction.LONG, offset=Offset.OPEN, price=6494.0, volume=3, traded=0, status=Status.SUBMITTING, datetime=CHINA_TZ.localize(datetime(2020, 8, 13, 21, 39, 53)), reference=''),
            OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_753490688_4', type=OrderType.LIMIT, direction=Direction.LONG, offset=Offset.OPEN, price=6494.0, volume=3, traded=3, status=Status.ALLTRADED, datetime=CHINA_TZ.localize(datetime(2020, 8, 13, 21, 39, 53)), reference=''),

            OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_1962356227_1', type=OrderType.LIMIT, direction=Direction.SHORT, offset=Offset.CLOSEYESTERDAY, price=5870.0, volume=12.0, traded=0, status=Status.SUBMITTING, datetime=None, reference='TTS-ag2012'),
            OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_1962356227_1', type=OrderType.LIMIT, direction=Direction.SHORT, offset=Offset.CLOSEYESTERDAY, price=5870.0, volume=12, traded=0, status=Status.SUBMITTING, datetime=CHINA_TZ.localize(datetime(2020, 8, 14, 1, 44,20)), reference=''),
            OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_1962356227_1', type=OrderType.LIMIT, direction=Direction.SHORT, offset=Offset.CLOSEYESTERDAY, price=5870.0, volume=12, traded=0, status=Status.SUBMITTING, datetime=CHINA_TZ.localize(datetime(2020, 8, 14, 1, 44,25)), reference=''),
            OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_1962356227_1', type=OrderType.LIMIT, direction=Direction.SHORT, offset=Offset.CLOSEYESTERDAY, price=5870.0, volume=12, traded=12, status=Status.ALLTRADED, datetime=CHINA_TZ.localize(datetime(2020, 8, 14, 1, 44,35)), reference=''),
        ]

        return orders


    # 开始测试代码
    app = create_qapp()

    symbol = "ag2012"
    exchange = Exchange.SHFE
    interval=Interval.MINUTE
    start=datetime(2020, 8, 13)
    end=datetime(2020, 8, 15)    

    dynamic = False  # 是否动态演示
    n = 1000          # 缓冲K线根数

    bars = database_manager.load_bar_data(
        symbol=symbol,
        exchange=exchange,
        interval=interval,
        start=start,
        end=end
    )

    print(f"一共读取{len(bars)}根K线")

    event_engine = EventEngine()

    widget = NewChartWidget(event_engine = event_engine)

    if dynamic:
        history = bars[:n]      # 先取得最早的n根bar作为历史
        new_data = bars[n:]     # 其它留着演示
    else:
        history = bars          # 先取得最新的n根bar作为历史
        new_data = []           # 演示的为空

    # 绘制历史K线主图及各个副图
    widget.update_history(history)

    # 绘制委托单到主图
    orders = make_orders()
    widget.add_orders(orders)

    # 绘制成交单到主图
    trades = make_trades()
    widget.add_trades(trades)

    def update_bar():
        if new_data:
            bar = new_data.pop(0)
            widget.update_bar(bar)

    timer = QtCore.QTimer()
    timer.timeout.connect(update_bar)
    if dynamic:
        timer.start(100)

    widget.show()

    event_engine.start()
    app.exec_()

4. 测试效果

kx_chart.py中自动测试代码,直接用VSCode打开就可以运行。

4.1 测试准备

在vnpy中使用数据管理模块,从米筐下载ag2012.SHFE的1分钟历史数据,必须包含8月13日~8月15日。

4.2 测试结果如下

description

1 OrderItem介绍

OrderItem是ChartWidget的一个主图绘制部件,它的功能:
1)在委托发生K线绘制委托单图形
2)当鼠标移动到委托单所在K线,在信息提示区提示成交的最新委托信息,注意:是最新的!

2 OrderItem的实现

OrderItem也是一个伪装成CandleItem的ScatterPlotItem。

class OrderItem(ScatterPlotItem,CandleItem): 
    """
    委托单绘图部件
    """
    def __init__(self, manager: BarManager):
        """"""
        ScatterPlotItem.__init__(self)
        super(CandleItem,self).__init__(manager)

        self.orders : Dict[int,Dict[str,Order]] = {} # {ix:{orderid:order}}

    def add_orders(self,orders:List[OrderData]):
        """ 增加委托单列表到OrderItem """
        for order in orders:
            if order.datetime:
                self.add_order(order)

        self.set_scatter_data()
        self.update()

    def add_order(self,order:OrderData,draw:bool=False):
        """ 增加一个委托单到OrderItem """
        # 这里使用reverse=True,是考虑到实盘成交往往发生在最新的bar里,可以加快搜索速度

        od = OrderedDict(sorted(self._manager._datetime_index_map.items(),key = lambda t:t[0],reverse=True))
        idx = self._manager.get_count() - 1
        for dt,ix in od.items():
            # print(f"dt={dt}\ntrade.datetime {trade.datetime}")
            dt1 = CHINA_TZ.localize(datetime.combine(dt.date(),dt.time()))
            if dt1 <= order.datetime:
                print(f"【dt={dt},dt1={dt1},dt2={order.datetime} ix={ix}】")
                idx = ix
                break

        # 注意:一个bar期间可能发生多个委托单
        if idx in self.orders:
            self.orders[idx][order.orderid] = order
        else:
            self.orders[idx] = {order.orderid:order}

        if draw:
            self.set_scatter_data()
            self.update()

    def set_scatter_data(self):
        """ 把委托单列表绘制到ScatterPlotItem上 """
        scatter_datas = []
        for ix in self.orders:
            lowest,highest=self.get_y_range()
            # print(f"range={lowest,highest}")
            for order in self.orders[ix].values():
                # 处理委托报价超出显示范围的问题
                if order.price>highest:
                    show_price = highest - 7
                elif order.price<lowest:
                    show_price = lowest + 7
                else:
                    show_price = order.price 

                scatter = {
                    "pos" : (ix, show_price),
                    "data": 1,
                    "size": 14,
                    "pen": pg.mkPen((255, 255, 255)),
                }

                if order.direction == Direction.LONG:
                    scatter_symbol = "t1"   # Up arrow
                else:
                    scatter_symbol = "t"    # Down arrow

                if order.offset == Offset.OPEN:
                    scatter_brush = pg.mkBrush((0, 128, 128))   # Yellow
                else:
                    scatter_brush = pg.mkBrush((128, 128, 0))     # Blue

                scatter["symbol"] = scatter_symbol
                scatter["brush"] = scatter_brush
                scatter_datas.append(scatter)

        self.setData(scatter_datas)

    def get_info_text(self, ix: int) -> str:
        """"""
        text = ""
        if ix in self.orders:
            for orderid,order in self.orders[ix].items():
                # OrderData
                text += f"委托:\n{order.price}{order.direction.value}{order.offset.value}{order.volume}手\n"

        return text

3 测试效果

description

3.1 关于委托单绘制的一点说明:

  1. 委托单中的时间为None的没有绘制,它发生在send_order()函数执行,交易服务器刚接受委托申请之时,此时还没有任何成交,也没有进入等待队列,只是一种委托请求的确认。因此无法找到其发生的K线位置,所以无法绘制。
  2. 委托单绘制的位置不总代表委托的价格,为了照顾到状态的显示,将超出显示区域最高和最低的委托价的委托单,放在显示区的顶部和底部来显示,信息提示板中的委托单的文字描述包含准确的委托价格。
  3. 当然,市价委托单通常都是在其实际委托价格处显示的。

请看看下面的委托单记录可以明白了:

        OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_753490688_1', type=OrderType.LIMIT, direction=Direction.LONG, offset=Offset.OPEN, price=6494.0, volume=3.0, traded=0, status=Status.SUBMITTING, datetime=None, reference='TTS-ag2012'),
        OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_753490688_1', type=OrderType.LIMIT, direction=Direction.LONG, offset=Offset.OPEN, price=6494.0, volume=3, traded=0, status=Status.SUBMITTING, datetime=CHINA_TZ.localize(datetime(2020, 8, 13, 21, 0, 1)), reference=''),
        OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_753490688_1', type=OrderType.LIMIT, direction=Direction.LONG, offset=Offset.OPEN, price=6494.0, volume=3, traded=0, status=Status.SUBMITTING, datetime=CHINA_TZ.localize(datetime(2020, 8, 13, 21, 0, 1)), reference=''),
        OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_753490688_1', type=OrderType.LIMIT, direction=Direction.LONG, offset=Offset.OPEN, price=6494.0, volume=3, traded=3, status=Status.ALLTRADED, datetime=CHINA_TZ.localize(datetime(2020, 8, 13, 21, 0, 1)), reference=''),

        OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_753490688_2', type=OrderType.LIMIT, direction=Direction.LONG, offset=Offset.OPEN, price=6494.0, volume=3.0, traded=0, status=Status.SUBMITTING, datetime=None, reference='TTS-ag2012'),
        OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_753490688_2', type=OrderType.LIMIT, direction=Direction.LONG, offset=Offset.OPEN, price=6494.0, volume=3, traded=0, status=Status.SUBMITTING, datetime=CHINA_TZ.localize(datetime(2020, 8, 13, 21, 14, 46)), reference=''),
        OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_753490688_2', type=OrderType.LIMIT, direction=Direction.LONG, offset=Offset.OPEN, price=6494.0, volume=3, traded=0, status=Status.SUBMITTING, datetime=CHINA_TZ.localize(datetime(2020, 8, 13, 21, 14, 46)), reference=''),
        OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_753490688_2', type=OrderType.LIMIT, direction=Direction.LONG, offset=Offset.OPEN, price=6494.0, volume=3, traded=3, status=Status.ALLTRADED, datetime=CHINA_TZ.localize(datetime(2020, 8, 13, 21, 14, 46)), reference=''),

        OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_753490688_3', type=OrderType.LIMIT, direction=Direction.LONG, offset=Offset.OPEN, price=6494.0, volume=3.0, traded=0, status=Status.SUBMITTING, datetime=None, reference='TTS-ag2012'),
        OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_753490688_3', type=OrderType.LIMIT, direction=Direction.LONG, offset=Offset.OPEN, price=6494.0, volume=3, traded=0, status=Status.SUBMITTING, datetime=CHINA_TZ.localize(datetime(2020, 8, 13, 21, 21, 59)), reference=''),
        OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_753490688_3', type=OrderType.LIMIT, direction=Direction.LONG, offset=Offset.OPEN, price=6494.0, volume=3, traded=0, status=Status.SUBMITTING, datetime=CHINA_TZ.localize(datetime(2020, 8, 13, 21, 21, 59)), reference=''),
        OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_753490688_3', type=OrderType.LIMIT, direction=Direction.LONG, offset=Offset.OPEN, price=6494.0, volume=3, traded=3, status=Status.ALLTRADED, datetime=CHINA_TZ.localize(datetime(2020, 8, 13, 21, 21, 59)), reference=''),

        OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_753490688_4', type=OrderType.LIMIT, direction=Direction.LONG, offset=Offset.OPEN, price=6494.0, volume=3.0, traded=0, status=Status.SUBMITTING, datetime=None, reference='TTS-ag2012'),
        OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_753490688_4', type=OrderType.LIMIT, direction=Direction.LONG, offset=Offset.OPEN, price=6494.0, volume=3, traded=0, status=Status.SUBMITTING, datetime=CHINA_TZ.localize(datetime(2020, 8, 13, 21, 39, 53)), reference=''),
        OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_753490688_4', type=OrderType.LIMIT, direction=Direction.LONG, offset=Offset.OPEN, price=6494.0, volume=3, traded=0, status=Status.SUBMITTING, datetime=CHINA_TZ.localize(datetime(2020, 8, 13, 21, 39, 53)), reference=''),
        OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_753490688_4', type=OrderType.LIMIT, direction=Direction.LONG, offset=Offset.OPEN, price=6494.0, volume=3, traded=3, status=Status.ALLTRADED, datetime=CHINA_TZ.localize(datetime(2020, 8, 13, 21, 39, 53)), reference=''),

        OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_1962356227_1', type=OrderType.LIMIT, direction=Direction.SHORT, offset=Offset.CLOSEYESTERDAY, price=5870.0, volume=12.0, traded=0, status=Status.SUBMITTING, datetime=None, reference='TTS-ag2012'),
        OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_1962356227_1', type=OrderType.LIMIT, direction=Direction.SHORT, offset=Offset.CLOSEYESTERDAY, price=5870.0, volume=12, traded=0, status=Status.SUBMITTING, datetime=CHINA_TZ.localize(datetime(2020, 8, 14, 1, 44,20)), reference=''),
        OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_1962356227_1', type=OrderType.LIMIT, direction=Direction.SHORT, offset=Offset.CLOSEYESTERDAY, price=5870.0, volume=12, traded=0, status=Status.SUBMITTING, datetime=CHINA_TZ.localize(datetime(2020, 8, 14, 1, 44,25)), reference=''),
        OrderData(gateway_name='CTP', symbol='ag2012', exchange=Exchange.SHFE, orderid='3_1962356227_1', type=OrderType.LIMIT, direction=Direction.SHORT, offset=Offset.CLOSEYESTERDAY, price=5870.0, volume=12, traded=12, status=Status.ALLTRADED, datetime=CHINA_TZ.localize(datetime(2020, 8, 14, 1, 44,35)), reference=''),

4 OrderItem的一点遗憾,有兴趣的可以帮忙改进

add_order()中在查找委托单所属bar的ix的时候,代码效率不高,当self._manager._datetime_index_map很大的时候,可能存在效率问题。
希望有兴趣的大侠可以帮忙改进,让后贡献vnpy社区,代表社区先行谢过!

5 OrderItem使用方法:

见:https://www.vnpy.com/forum/topic/4621-wei-kxian-tu-biao-tian-zhuan-jia-wa-yi-ge-wan-zheng-de-kxian-tu-biao

1 TradeItem介绍

TradeItem是ChartWidget的一个主图绘制部件,它的功能:
1)在成交单发生K线绘制成交单图形
2)当鼠标移动到成交单所在K线,在信息提示区提示成交的成交信息

2 TradeItem的实现

它是一个伪装成CandleItem的ScatterPlotItem。

class TradeItem(ScatterPlotItem,CandleItem): 
    """
    成交单绘图部件
    """
    def __init__(self, manager: BarManager):
        """"""
        ScatterPlotItem.__init__(self)
        # CandleItem.__init__(self,manager)
        # super(TradeItem,self).__init__(manager)
        super(CandleItem,self).__init__(manager)


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

        self.trades : Dict[int,Dict[str,TradeData]] = {} # {ix:{tradeid:trade}}

    def add_trades(self,trades:List[TradeData]):
        """ 增加成交单列表到TradeItem """
        for trade in trades:
            self.add_trade(trade)

        self.set_scatter_data()
        self.update()

    def add_trade(self,trade:TradeData,draw:bool=False):
        """ 增加一个成交单到TradeItem """
        # 这里使用reverse=True,是考虑到实盘成交往往发生在最新的bar里,可以加快搜索速度
        od = OrderedDict(sorted(self._manager._datetime_index_map.items(),key = lambda t:t[0],reverse=True))
        idx = self._manager.get_count() - 1
        for dt,ix in od.items():
            # print(f"dt={dt}\ntrade.datetime {trade.datetime}")
            dt1 = CHINA_TZ.localize(datetime.combine(dt.date(),dt.time()))
            if dt1 <= trade.datetime:
                # print(f"【dt={dt},dt1={dt1},dt2={trade.datetime} ix={ix}】")
                idx = ix
                break

        # 注意:一个bar期间可能发生多个成交单
        if idx in self.trades:
            self.trades[idx][trade.tradeid] = trade
        else:
            self.trades[idx] = {trade.tradeid:trade}

        if draw:        
            self.set_scatter_data()
            self.update()

        # print(f"add_trade idx={idx} trade={trade}")

    def set_scatter_data(self):
        """ 把成交单列表绘制到ScatterPlotItem上 """
        scatter_datas = []
        for ix in self.trades:
            for trade in self.trades[ix].values():
                scatter = {
                    "pos" : (ix, trade.price),
                    "data": 1,
                    "size": 14,
                    "pen": pg.mkPen((255, 255, 255)),
                }

                if trade.direction == Direction.LONG:
                    scatter_symbol = "t1"   # Up arrow
                else:
                    scatter_symbol = "t"    # Down arrow

                if trade.offset == Offset.OPEN:
                    scatter_brush = pg.mkBrush((255, 255, 0))   # Yellow
                else:
                    scatter_brush = pg.mkBrush((0, 0, 255))     # Blue

                scatter["symbol"] = scatter_symbol
                scatter["brush"] = scatter_brush
                scatter_datas.append(scatter)

        self.setData(scatter_datas)

    def get_info_text(self, ix: int) -> str:
        """"""
        text = ""
        if ix in self.trades:
            for tradeid,trade in self.trades[ix].items():
                # TradeData
                text += f"成交:\n{trade.price}{trade.direction.value}{trade.offset.value}{trade.volume}手\n"
        # else:
        #     text = f"{self.__class__}"

        return text

3 测试效果

description

4 TradeItem的一点遗憾,有兴趣的可以帮忙改进

add_trade()中在查找成交单所属bar的ix的时候,代码效率不高,当self._manager._datetime_index_map很大的时候,可能存在效率问题。
希望有兴趣的大侠可以帮忙改进,然后贡献vnpy社区,代表社区先行谢过!

5 TradeItem使用方法:

见:https://www.vnpy.com/forum/topic/4621-wei-kxian-tu-biao-tian-zhuan-jia-wa-yi-ge-wan-zheng-de-kxian-tu-biao

1 vnpy 2.1.4以后的系统中使用的CHINA_TZ 的时间标准

现在绝大部分模块用、应用都已经对使用到的时间做了本地化处理,因此,如果你有一个新构造的datetime变量,应该先在使用CHINA_TZ.localize(),然后再使用,就会避免因为时区不同导致的时间比较上的错误。方法如下:
import pytz
CHINA_TZ = pytz.timezone("Asia/Shanghai")
CHINA_TZ.localize(dt_var)

2 数据下载模块中下载的数据中的时间没有做本地化处理

数据下载模块从米筐下载的数据,下载到本地本地供仿真和实盘使用,可是vnpy没有对其数据中的时间做本地化处理,直接使用会有问题。
虽然他们看来是一样的时间刻度,可和实盘中的时间下单和成交时间有几分钟的差距,
同样的时间面值,因为时区的不同导致比较得不到想要的结果:
下面的dt1和dt2是为把数据库中的时间做比较得到,其中:

dt1是数据库中数据的时间,它们的时区是+08:06,
dt2是成交单中数据的时间,它们的时区是+08:00

【dt1=2020-08-13 21:00:00+08:06,dt2=2020-08-13 21:00:01+08:00 ix=375】
【dt1=2020-08-13 21:14:00+08:06,dt2=2020-08-13 21:14:46+08:00 ix=389】
【dt1=2020-08-13 21:21:00+08:06,dt2=2020-08-13 21:21:59+08:00 ix=396】
【dt1=2020-08-13 21:39:00+08:06,dt2=2020-08-13 21:39:53+08:00 ix=414】
【dt1=2020-08-14 01:44:00+08:06,dt2=2020-08-14 01:44:35+08:00 ix=659】

它们带来的问题是
1)不可以直接比较,必须先转化为相同是时区配置才可以比较
2)很迷糊人,看似相同的时间,可重新就是判断是不想等

3 建议vnpy官方把数据下载模块中的时间统一本地化标准

1. 先看看CandleChartDialog的代码

class CandleChartDialog(QtWidgets.QDialog):
    """
    """

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

        self.dt_ix_map = {}
        self.updated = False
        self.init_ui()

    def init_ui(self):
        """"""
        self.setWindowTitle("回测K线图表")
        self.resize(1400, 800)

        # Create chart widget
        self.chart = ChartWidget()
        self.chart.add_plot("candle", hide_x_axis=True)
        self.chart.add_plot("volume", maximum_height=200)
        self.chart.add_item(CandleItem, "candle", "candle")
        self.chart.add_item(VolumeItem, "volume", "volume")
        self.chart.add_cursor()

        # Add scatter item for showing tradings
        self.trade_scatter = pg.ScatterPlotItem()
        candle_plot = self.chart.get_plot("candle")
        candle_plot.addItem(self.trade_scatter)

        # Set layout
        vbox = QtWidgets.QVBoxLayout()
        vbox.addWidget(self.chart)
        self.setLayout(vbox)

    def update_history(self, history: list):
        """"""
        self.updated = True
        self.chart.update_history(history)

        for ix, bar in enumerate(history):
            self.dt_ix_map[bar.datetime] = ix

    def update_trades(self, trades: list):
        """"""
        trade_data = []

        for trade in trades:
            ix = self.dt_ix_map[trade.datetime]

            scatter = {
                "pos": (ix, trade.price),
                "data": 1,
                "size": 14,
                "pen": pg.mkPen((255, 255, 255))
            }

            if trade.direction == Direction.LONG:
                scatter_symbol = "t1"   # Up arrow
            else:
                scatter_symbol = "t"    # Down arrow

            if trade.offset == Offset.OPEN:
                scatter_brush = pg.mkBrush((255, 255, 0))   # Yellow
            else:
                scatter_brush = pg.mkBrush((0, 0, 255))     # Blue

            scatter["symbol"] = scatter_symbol
            scatter["brush"] = scatter_brush

            trade_data.append(scatter)

        self.trade_scatter.setData(trade_data)

    def clear_data(self):
        """"""
        self.updated = False
        self.chart.clear_all()

        self.dt_ix_map.clear()
        self.trade_scatter.clear()

    def is_updated(self):
        """"""
        return self.updated

2. 再看看self.trade_scatter成员

2.1 一个特别的成员self.trade_scatter

它和其他的绘图部件不同,其他的都是ChartItem类型继承得到,只有它例外,它是一个ScatterPlotItem。

self.trade_scatter = pg.ScatterPlotItem()

2.2 问题:CandleChartDialog不可以在非回测环境下使用

看看update_trades() 的代码中

for trade in trades:
     ix = self.dt_ix_map[trade.datetime]   # 查找一个成交单(trade)是属于哪个BarData的索引

这里能够不出错,完全是因为在回测中,人为固定地把发出交易信号的那个bar的开始时间datetime赋值给了trade.datetime!
让我们来看看app\cta_strategy\backtesting.py中的class BacktestingEngine,它在bar模式的时候,是这样生成成交单的:

            trade = TradeData(
                symbol=order.symbol,
                exchange=order.exchange,
                orderid=order.orderid,
                tradeid=str(self.trade_count),
                direction=order.direction,
                offset=order.offset,
                price=trade_price,
                volume=order.volume,
                datetime=self.datetime,
                gateway_name=self.gateway_name,
            )

其中trade中的datetime=self.datetime,而self.datetime是这样赋值的:

    def run_backtesting(self):
        """"""
        if self.mode == BacktestingMode.BAR:
            func = self.new_bar
        else:
            func = self.new_tick

        self.strategy.on_init()

        # Use the first [days] of history data for initializing strategy
        day_count = 1
        ix = 0

        for ix, data in enumerate(self.history_data):
            if self.datetime and data.datetime.day != self.datetime.day:
                day_count += 1
                if day_count >= self.days:
                    break

            self.datetime = data.datetime      # 它是用self.history_data中代表bar的data.datetime赋值的!
            ... ...

可是我们知道trade.datetime是不可能总是恰好等于bar的开始时间datetime的,成交时间可能是一个bar形成期间的任何时间。
回到CandleChartDialog,由上面可知self.dt_ix_map的维护是update_history()维护的,它是由bar的开始时间datetime和其顺序ix构成的一个字典。

2.3 如果trade是实际的成交单,使用CandleChartDialog是一定会出错

原因是trade.datetime在self.dt_ix_map字典的键值中大概率是不存在的,因此ix = self.dt_ix_map[trade.datetime]语句会出错!而ScatterPlotItem是通过"pos": (ix, trade.price)来确定代表买卖的上下三角形来绘图的,因此CandleChartDialog是不可以简单地在显示实盘成交单的地方引用的。

3. 怎么修改既可显示回测成交单,又可以显示实盘成交单办?

思路:

3.1 增加如下成员:

     self.trades:Dict[int,Dict[str,TradeData]] = {}  # 其键值为成交发生bar的索引,内容为成交单字典

3.2 增加add_trade():

根据trade.datetime字段在self.dt_ix_map中所在位置的两个相邻时间dt0和dt1
满足:

def add_trade(self,trade:TradeData):
  # 这里使用reverse=True,是考虑到实盘成交往往发生在最新的bar里,可以加快搜索速度
  od = OrderedDict(sorted(self.dt_ix_map.items(),key = lambda t:t[0],reverse=True))
  idx = 0
  for dt,ix in od.items():
    if dt <= trade.datetime:
        idx = idx
        break
  # 注意:一个bar期间可能发生多个成交单
  if idx not in self.trades:
      self.trades[idx] = {trade.tradeid: trade}
  else:
      self.trades[idx][trade.tradeid] = trade

3.3 update_trades()这么修改就OK了。

    def update_trades(self, trades: list):
        """"""
        for trade in trades:
             # 寻找每个成交单对应的bar的索引,并且保证为字典
             self.add_trade(trade)

        trade_data = []
        for ix in self.trades:
            for tradeid,trade in self.trades[ix].items():
                scatter = {
                    "pos": (ix, trade.price),
                    "data": 1,
                    "size": 14,
                    "pen": pg.mkPen((255, 255, 255))
                }

                if trade.direction == Direction.LONG:
                    scatter_symbol = "t1"   # Up arrow
                else:
                    scatter_symbol = "t"    # Down arrow

                if trade.offset == Offset.OPEN:
                    scatter_brush = pg.mkBrush((255, 255, 0))   # Yellow
                else:
                    scatter_brush = pg.mkBrush((0, 0, 255))     # Blue

                scatter["symbol"] = scatter_symbol
                scatter["brush"] = scatter_brush

                trade_data.append(scatter)

        self.trade_scatter.setData(trade_data)

4. 不一样的成交单显示解决办法

参见:典型绘图部件及使用方法

1 BarData的定义如下:

@dataclass
class BarData(BaseData):
    """
    Candlestick bar data of a certain trading period.
    """

    symbol: str
    exchange: Exchange
    datetime: datetime

    interval: Interval = None
    volume: float = 0
    open_interest: float = 0
    open_price: float = 0
    high_price: float = 0
    low_price: float = 0
    close_price: float = 0

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

2 BarData没有表示时间宽度的成员

2.1 里面的成员没有能够区分Bar长度的

datetime:是BarData的开始时间
interval的类型是Interval枚举值,应该是时间单位,取值只能够是:
MINUTE = "1m":表示1分钟
HOUR = "1h":表示1小时
DAILY = "d":表示1日
WEEKLY = "w":表示1周

2.2 问题来了,如何区分5分钟BarData线和30分钟BarData?

如果给你两个BarData对象,怎么区分它是5分钟的和30分钟的呢?它们的Interval只能为MINUTE,也不可能是其他的更大的单位。这样它们岂不是都长的一个样了吗?
是否还需要增加表示时长的width或者表示结束时间的endtime来作为区分呢?
如果是使用width的话,可以这样表示
datetime='2020-9-8 9:00:00',interval = Interval.MINUTE,width=5 —— 5分钟
datetime='2020-9-8 9:00:00',interval = Interval.MINUTE,width=15 —— 5分钟
datetime='2020-9-8 9:00:00',interval = Interval.MINUTE,width=30 —— 30分钟
如果使用endtime的话,可以这样表示:(interval参数就有些多余)
datetime='2020-9-8 9:00:00',endtime='2020-9-8 9:05:00',interval = Interval.MINUTE —— 5分钟
datetime='2020-9-8 9:00:00',endtime='2020-9-8 9:15:00',interval = Interval.MINUTE —— 5分钟
datetime='2020-9-8 9:00:00',endtime='2020-9-8 9:30:00',interval = Interval.MINUTE —— 30分钟

1 在VSCode中安装Compare Folders插件

  1. 点击设置
  2. 输入‘Compare’查找
  3. Install安装Compare Folders插件
    description

2 把要比较的目录添加到VSCode的工作区

  1. 安装插件成功后在设置按钮下会多出一个目录比较的按钮
  2. 为当前工作区添加一个目录
  3. 再次为当前工作区添加一个目录
  4. 按住Ctrl键,把要比较的目录都选中,点击鼠标右键,进行目录比较操作
    description

3 比较两个目录中的文件内容

3.1 名词:

MY FOLDER :我的目录
COMPARED FOLDER:被比较目录

3.2 下图共分5个区:

  1. 参与比较的目录都但是有差别的文件
  2. 只在我的目录中有,而被比较目录中没有的文件
  3. 只在被比较目录中有,而我的目录中没有的文件
  4. 被比较目录的文件内容
  5. 我的目录的文件内容

description

1 使用CTP测试接口进行认证前的评测,总是出错!

开户在中信建投期货,申请做CTP认证,填了个表格发过去。第二天说要先做CTP认证测试。好,做吧!

1.1 启动VNStation,进入下面的界面:

网关选择CTP测试
description

1.2 输入评测参数

输入测评给用户名、密码、经纪商代码、交易服务器ip及端口、行情服务器ip及端口 和 认证码等项:
description

1.3 登录失败报出4097

按连接按钮,却出现下面的错误,连续3天解决不了。也不知道时期货公司的问题,还是咱自己的问题。
description

2 重新安装,进行CTP评测登录,没有问题!

找到一台未安装过vnpy的电脑,安装vnpy后直接重复上面的步骤,OK,一次就通过测评了!

3 寻找问题的根源,原来时CtpGateway被同时加载了

3.1 在vnstation\info.py中有下面的叙述:

TEXT_TRADER_CONFIG = """
1. 请勾选需要使用的底层接口和上层应用模块
2. 配置完毕后,点击“启动”按钮来打开VN Trader
3. VN Trader运行时请勿关闭VN Station(会造成退出)
4. CTP、CTP测试接口不能同时加载(会导致API版本错误)
5. CTP Mini和CTP Mini测试接口不能同时加载(会导致API版本错误)
"""

3.2 猜测是修改vnpy系统的过程中,不小心加载了CtpGateway

那就找到vnpy\gateway\ctp__init__.py,按照下面修改:

from .ctp_gateway import CtpGateway

# 增加主动出错定位的代码
print("【CtpGateway inited】")
abc = not_exist_var # 制造一个错误,以便发现错误

3.3 根据错误提示,找到错误位置

再次在VNStation Prompt窗口中输入 python -m vnstation命令,启动VNStation,如果不能顺利启动到主界面,并且出现了错误,说明你的修改导致了CtpGateway模块被引用过(因为我们制作了一个错误,只要你加载了CTP模块,就会出错!),根据报出的出错语句,你就可以顺利地将问题找到。目标就是不可以在没有寻找CTP网关接口的情况下,加载CTP模块,from,import语句是最容易出错的地方,
如:
from vnpy.gateway.ctp.ctp_gateway import xxxxx
哪怕是import的符号不是CtpGateway也不可以。

3.4 排查出问题后恢复vnpy\gateway\ctp__init__.py的内容:

把制造错误的语句注释掉。

from .ctp_gateway import CtpGateway

# 增加主动出错定位的代码
print("【CtpGateway inited】")
# abc = not_exist_var # 制造一个错误,以便发现错误

3.5 再次选择CTP测试网关,填写之前的测评设置参数,就可以顺利登录期货公司的测评环境了。

网关接口的状态感知

1 行情和交易服务器的接口状态

网关分很多种,如CTP,XTP等 。其中CTP中又包含行情(MD)接口和交易(TD)接口。它们在连接和断开的时候,都有推送接口。这些接口是:
MD的onFrontConnected()和onFrontDisconnected(),TD的onFrontConnected()和onFrontDisconnected()。行情和交易服务器的接口状态,vnpy已经做了log输出,用户是可以阅读的,可是不方便软件使用,于是对接口和主引擎做了如下修改,以便于上层应用可以使用这些消息,编写出针对行情服务器通断情况的处理方法,也可以编写针对交易服务器连接的处理方法。

2 实现步骤

下面的代码修改时,需要相互引用的部分,如from... import... 之类的,就不再逐一指出了,太基础了。

2.1 在vnpy\trader\event.py添加如下消息类型:

EVENT_CONNECT = "eConnected"             # hxxjava add
EVENT_DISCONNECT = "eDisconnected"   # hxxjava add

2.2 在vnpy\trader\object.py添加如下数据类型:

@dataclass
class GatewayData():     # hxxjava add
    """
    Gateway data
    """
    name:str = ""     # 网关名称,如 'CTP'
    type:str = ""     # 接口类型,如 'TD','MD'
    reason:int = 0      # 状态或者原因

2.3 在vnpy\trader\gateway.py 中为BaseGateway添加如下函数:

    def on_connect(self,gateway:GatewayData) -> None:    # hxxjava add   
        """
        gateway connect enent
        """
        self.on_event(EVENT_CONNECT, gateway)

    def on_disconnect(self,gateway:GatewayData) -> None:    # hxxjava add   
        """
        gateway disconnect enent
        """
        self.on_event(EVENT_DISCONNECT, gateway)

2.4 在vnpy\app\ctp\ctp_gateway.py 中修改如下4个函数:

1 CtpMdApi类的两个函数:

    def onFrontConnected(self):
        """
        Callback when front server is connected.
        """
        self.gateway.on_connect(GatewayData("CTP",'MD'))    # hxxjava add

        self.gateway.write_log("行情服务器连接成功")
        self.login()

    def onFrontDisconnected(self, reason: int):
        """
        Callback when front server is disconnected.
        """
        self.login_status = False
        self.gateway.on_disconnect(GatewayData(name="CTP",type='MD',reason=reason)) # hxxjava add
        self.gateway.write_log(f"行情服务器连接断开,原因{reason}")

2 CtpTdApi类的两个函数:

    def onFrontConnected(self):
        """"""
        self.gateway.on_connect(GatewayData("CTP",'TD')) # hxxjava add
        self.gateway.write_log("交易服务器连接成功")

        if self.auth_code:
            self.authenticate()
        else:
            self.login()

    def onFrontDisconnected(self, reason: int):
        """"""
        self.login_status = False
        self.gateway.on_disconnect(GatewayData(name="CTP",type='TD',reason=reason)) # hxxjava add
        self.gateway.write_log(f"交易服务器连接断开,原因{reason}")

2.5 修改vnpy\trader\engine.py中的主引擎MainEngine

修改register_event()函数:

    def register_event(self):
        ... ...
        # 添加下面两句 
        self.event_engine.register(EVENT_CONNECT, self.process_connect_event)
        self.event_engine.register(EVENT_DISCONNECT, self.process_disconnect_event)

添加下面两个消息处理函数:

    def process_connect_event(self, event: Event) -> None:
        gateway:GatewayData = event.data
        print(f"gateway connect event {gateway}")

    def process_disconnect_event(self, event: Event) -> None:
        gateway:GatewayData = event.data
        print(f"gateway disconnect_event {gateway}")

3 如何测试

在VN Studio Prompt窗口输入python - m vnstation命令,启动vnpy系统后,选择连接CTP接口,输入用户名和密码等信息后,连接CTP网关后可以在VN Studio Prompt窗口见到如下:

gateway connect event GatewayData(name='CTP', type='TD', reason=0)
gateway connect event GatewayData(name='CTP', type='MD', reason=0)

当然你也可以制造一下CTP网关的故障,如故意拔掉你的路由器的电,或者网线,你应该可以看到

gateway disconnect event GatewayData(name='CTP', type='TD', reason=?)   
gateway disconnect event GatewayData(name='CTP', type='MD', reason=?)

上面使用问号,是因为我不知道你软件会提示什么错误原因。

1. algo应用和CTA应用增加用户策略方法不一样

  • 新建CTA用户策略只要在 [用户命令]\strategies\目录下,创建自己的策略,实现on_start ,on_init, on_tick, on_bar, on_trade, on_order, on_stop_order等推送接口。重新加载cta app,vnpy系统会自动把系统自带策略和用户策略一起编译添加进CTA应用界面,直接选择就OK;
  • algo应用却不是这样的,它的策略是在algo引擎初始化时候,执行load_algo_template()加载的,加载哪些策略是写死的,它没有像CTA app那样考虑用户策略的加载,代码如下:
    def load_algo_template(self):
        """"""
        from .algos.twap_algo import TwapAlgo
        from .algos.iceberg_algo import IcebergAlgo
        from .algos.sniper_algo import SniperAlgo
        from .algos.stop_algo import StopAlgo
        from .algos.best_limit_algo import BestLimitAlgo
        from .algos.grid_algo import GridAlgo
        from .algos.dma_algo import DmaAlgo
        from .algos.arbitrage_algo import ArbitrageAlgo
        from .algos.test_algo import TestAlgo       # hxxjava add

        self.add_algo_template(TwapAlgo)
        self.add_algo_template(IcebergAlgo)
        self.add_algo_template(SniperAlgo)
        self.add_algo_template(StopAlgo)
        self.add_algo_template(BestLimitAlgo)
        self.add_algo_template(GridAlgo)
        self.add_algo_template(DmaAlgo)
        self.add_algo_template(ArbitrageAlgo)
        self.add_algo_template(TestAlgo)    # hxxjava add

        from .genus import (
            GenusVWAP,
            GenusTWAP,
            GenusPercent,
            GenusPxInline,
            GenusSniper,
            GenusDMA
        )

        self.add_algo_template(GenusVWAP)
        self.add_algo_template(GenusTWAP)
        self.add_algo_template(GenusPercent)
        self.add_algo_template(GenusPxInline)
        self.add_algo_template(GenusSniper)
        self.add_algo_template(GenusDMA)

考虑到这一特点,只能像上面带注释的行这么添加algo策略。

2. 复制Iceberg策略代码,修改成TestAlgo策略

from vnpy.trader.constant import Offset, Direction
from vnpy.trader.object import TradeData, OrderData, TickData
from vnpy.trader.engine import BaseEngine

from vnpy.app.algo_trading import AlgoTemplate


class TestAlgo(AlgoTemplate):
    """"""

    display_name = "TestAlgo 测试算法"

    default_setting = {
        "vt_symbol": "",
        "direction": [Direction.LONG.value, Direction.SHORT.value],
        "price": 0.0,
        "volume": 0.0,
        "display_volume": 0.0,
        "interval": 0,
        "offset": [
            Offset.NONE.value,
            Offset.OPEN.value,
            Offset.CLOSE.value,
            Offset.CLOSETODAY.value,
            Offset.CLOSEYESTERDAY.value
        ]
    }

    variables = [
        "traded",
        "timer_count",
        "vt_orderid"
    ]

    def __init__(
        self,
        algo_engine: BaseEngine,
        algo_name: str,
        setting: dict
    ):
        """"""
        super().__init__(algo_engine, algo_name, setting)

        # Parameters
        self.vt_symbol = setting["vt_symbol"]
        self.direction = Direction(setting["direction"])
        self.price = setting["price"]
        self.volume = setting["volume"]
        self.display_volume = setting["display_volume"]
        self.interval = setting["interval"]
        self.offset = Offset(setting["offset"])

        # Variables
        self.timer_count = 0
        self.vt_orderid = ""
        self.traded = 0

        self.last_tick = None

        self.subscribe(self.vt_symbol)
        self.put_parameters_event()
        self.put_variables_event()

    def on_stop(self):
        """"""
        self.write_log("停止算法")

    def on_tick(self, tick: TickData):
        """"""
        self.last_tick = tick

    def on_order(self, order: OrderData):
        """"""
        msg = f"委托号:{order.vt_orderid},委托状态:{order.status.value}"
        self.write_log(msg)

        if not order.is_active():
            self.vt_orderid = ""
            self.put_variables_event()

    def on_trade(self, trade: TradeData):
        """"""
        self.traded += trade.volume

        if self.traded >= self.volume:
            self.write_log(f"已交易数量:{self.traded},总数量:{self.volume}")
            self.stop()
        else:
            self.put_variables_event()

    def on_timer(self):
        """"""
        self.timer_count += 1

        if self.timer_count < self.interval:
            self.put_variables_event()
            return

        self.timer_count = 0

        contract = self.get_contract(self.vt_symbol)
        if not contract:
            return

        print(f"last_tick={self.last_tick}")

        # # If order already finished, just send new order
        # if not self.vt_orderid:
        #     order_volume = self.volume - self.traded
        #     order_volume = min(order_volume, self.display_volume)

        #     if self.direction == Direction.LONG:
        #         self.vt_orderid = self.buy(
        #             self.vt_symbol,
        #             self.price,
        #             order_volume,
        #             offset=self.offset
        #         )
        #     else:
        #         self.vt_orderid = self.sell(
        #             self.vt_symbol,
        #             self.price,
        #             order_volume,
        #             offset=self.offset
        #         )
        # # Otherwise check for cancel
        # else:
        #     if self.direction == Direction.LONG:
        #         if self.last_tick.ask_price_1 <= self.price:
        #             self.cancel_order(self.vt_orderid)
        #             self.vt_orderid = ""
        #             self.write_log(u"最新Tick卖一价,低于买入委托价格,之前委托可能丢失,强制撤单")
        #     else:
        #         if self.last_tick.bid_price_1 >= self.price:
        #             self.cancel_order(self.vt_orderid)
        #             self.vt_orderid = ""
        #             self.write_log(u"最新Tick买一价,高于卖出委托价格,之前委托可能丢失,强制撤单")

        self.put_variables_event()

3. 重新启动algo就可以看见 ”TestAlgo 测试算法“啦

description

1. 计算下一个节假日的起止日期

    def get_next_holiday(self,dt:datetime):
        """ 
        计算下一个节假日的起止日期
        注意:dt必须为交易时间,否则返回为(0,(None,None))
        """
        days,holday_start,holday_stop = 0,None,None
        index,trade_times = self.get_trade_datetimes(dt)
        if trade_times:
            # dt在交易时间段内
            day_count = len(self.trade_index_date)
            for idx in range(index,day_count-1):
                # 寻找间隔超过一天的前后两个交易日
                date1 = self.trade_index_date[idx]
                date2 = self.trade_index_date[idx+1]
                date_diff = date2 - date1
                if date_diff > datetime.timedelta(days=1):
                    # 找到了间隔超过一天的前后两个交易日
                    days = date_diff.days - 1
                    # 前1日加一天为节假日开始
                    holday_start = date1 + datetime.timedelta(days=1)
                    # 后1日减一天为节假日结束
                    holday_stop = date2 + datetime.timedelta(days=-1)
                    break

        return days,(holday_start,holday_stop)

2. 计算交易时间距离节假日的时间

    def to_holiday_time(self,dt:datetime):
        """
        计算交易时间距离节假日的时间。
        无夜盘合约 : 到节假日前一交易日的日盘收盘时间
        有夜盘合约 : 到节假日后一交易日的夜盘收盘时间
        注意:dt必须为交易时间,否则返回为None
              返回值:
        """
        index,trade_times = self.get_trade_datetimes(dt,True)
        if trade_times:
            # dt在交易时间段内
            day_count = len(self.trade_index_date)
            for idx in range(index,day_count-1):
                # 寻找间隔超过一天的前后两个交易日
                date1 = self.trade_index_date[idx]
                date2 = self.trade_index_date[idx+1]
                date_diff = date2 - date1
                if date_diff > datetime.timedelta(days=1):
                    if not self.has_night_trading:
                        days,trade_times = self.get_date_tradetimes(date1)
                        stop_dt = trade_times[-1][1]
                        # print(f"{stop_dt} ")
                    else:
                        days,trade_times = self.get_date_tradetimes(date2)
                        dt1 = trade_times[0][0] 
                        has,(start_dt,stop_dt) = self.get_night_start_stop(dt1)
                        # print(f"dt1={dt1},{(start_dt,stop_dt)} ")

                    # print(f"has_night_trading ={self.has_night_trading}, stop_dt={stop_dt}")  
                    time_diff = stop_dt - dt
                    return time_diff
        return None

3. 完整的交易时间段类实现

保存文件:vnpy\usertools\trade_hour.py

"""
本文件主要实现合约的交易时间段
作者:hxxjava
日期:2020-8-1
"""
from typing import Callable,List,Dict, Tuple, Union
from enum import Enum

import datetime
import pytz
CHINA_TZ = pytz.timezone("Asia/Shanghai")

from vnpy.trader.utility import extract_vt_symbol
from vnpy.trader.constant import Interval

from rqdatac.utils import to_date
import rqdatac as rq


def get_listed_date(symbol:str):
    ''' 
    获得上市日期 
    '''
    info = rq.instruments(symbol)
    return to_date(info.listed_date)

def get_de_listed_date(symbol:str):
    ''' 
    获得交割日期 
    '''
    info = rq.instruments(symbol)
    return to_date(info.de_listed_date)

class Timeunit(Enum):
    """ 
    时间单位 
    """
    SECOND = '1s'
    MINUTE = '1m'
    HOUR = '1h'

class TradeHours(object):
    """ 合约交易时间段 """
    def __init__(self,symbol:str):
        self.symbol = symbol.upper()
        self.init()

    def init(self):
        """ 
        初始化交易日字典及交易时间段数据列表 
        """
        self.listed_date = get_listed_date(self.symbol)
        self.de_listed_date = get_de_listed_date(self.symbol)

        self.trade_date_index = {}   # 合约的交易日索引字典
        self.trade_index_date = {}   # 交易天数与交易日字典

        trade_dates = rq.get_trading_dates(self.listed_date,self.de_listed_date) # 合约的所有的交易日
        days = 0
        for td in trade_dates:
            self.trade_date_index[td] = days
            self.trade_index_date[days] = td
            days += 1

        trading_hours = rq.get_trading_hours(self.symbol,date=self.listed_date,frequency='tick',expected_fmt='datetime')

        self.has_night_trading = False
        self.time_dn_pairs = self._get_trading_times_dn(trading_hours)

        trading_hours0 = [(CHINA_TZ.localize(start),CHINA_TZ.localize(stop)) for start,stop in trading_hours]
        self.trade_date_index[self.listed_date] = (0,trading_hours0)
        for day in range(1,days):
            td = self.trade_index_date[day]
            trade_datetimes = []
            for (start,dn1),(stop,dn2) in self.time_dn_pairs:
                #start:开始时间,dn1:相对交易日前推天数,
                #stop :开始时间,dn2:相对开始时间后推天数     
                d = self.trade_index_date[day+dn1]
                start_dt = CHINA_TZ.localize(datetime.datetime.combine(d,start))
                stop_dt = CHINA_TZ.localize(datetime.datetime.combine(d,stop))
                trade_datetimes.append((start_dt,stop_dt+datetime.timedelta(days=dn2)))
            self.trade_date_index[td] = (day,trade_datetimes)

    def _get_trading_times_dn(self,trading_hours:List[Tuple[datetime.datetime,datetime.datetime]]): 
        """ 
        交易时间跨天处理,不推荐外部使用 。
        产生的结果:[((start1,dn11),(stop1,dn21)),((start2,dn12),(stop2,dn22)),...,((startN,dn1N),(stopN,dn2N))]
        其中:
            startN:开始时间,dn1N:相对交易日前推天数,
            stopN:开始时间,dn2N:相对开始时间后推天数      
        """
        ilen = len(trading_hours)
        if ilen == 0:
            return []
        start_stops = []
        for start,stop in trading_hours:
            start_stops.insert(0,(start.time(),stop.time()))

        pre_start,pre_stop = start_stops[0]
        dn1 = 0
        dn2 = 1 if pre_start > pre_stop else 0
        time_dn_pairs = [((pre_start,dn1),(pre_stop,dn2))]

        for start,stop in start_stops[1:]:
            if start > pre_start:
                self.has_night_trading = True
                dn1 -= 1
            dn2 = 1 if start > stop else 0
            time_dn_pairs.insert(0,((start,dn1),(stop,dn2)))
            pre_start,pre_stop = start,stop

        return time_dn_pairs

    def get_date_tradetimes(self,date:datetime.date):
        """ 
        得到合约date日期的交易时间段 
        """
        idx,trade_times = self.trade_date_index.get(date,(None,[]))
        return idx,trade_times

    def get_trade_datetimes(self,dt:datetime,allday:bool=False):
        """ 
        得到合约date日期的交易时间段 
        """
        # 得到最早的交易时间
        idx0,trade_times0 = self.get_date_tradetimes(self.listed_date)
        start0,stop0 = trade_times0[0]
        if dt < start0:
            return None,[]

        # 首先找到dt日期自上市以来的交易天数
        date,dn = dt.date(),0
        days = None
        while date < self.de_listed_date:
            days,ths = self.trade_date_index.get(date,(None,[]))
            if not days:
                dn += 1
                date = (dt+datetime.timedelta(days=dn)).date()
            else:
                break
        # 如果超出交割日也没有找到,那这就不是一个有效的交易时间
        if days is None:
            return (None,[])

        index_3 = [days,days+1,days-1]  # 前后三天的

        date_3d = []
        for day in index_3: 
            date = self.trade_index_date.get(day,None)
            date_3d.append(date)

        # print(date_3d)

        for date in date_3d:
            if not date:
                # print(f"{date} is not trade date")
                continue

            idx,trade_dts = self.get_date_tradetimes(date)
            # print(f"{date} tradetimes {trade_dts}")
            ilen = len(trade_dts)
            if ilen > 0:
                start0,stop = trade_dts[0]      # start0 是date交易日的开始时间
                start,stop0 = trade_dts[-1]
            if dt<start0 or dt>stop0:
                continue

            for start,stop in trade_dts:
                if dt>=start and dt < stop:
                    if allday:
                        return idx,trade_dts
                    else:
                        return idx,[(start,stop)]

        return None,[]

    def get_trade_time_perday(self):
        """ 
        计算每日的交易总时长(单位:分钟) 
        """
        TTPD = datetime.timedelta(0,0,0)

        datetimes = []
        today = datetime.datetime.now().date()

        for (start,dn1),(stop,dn2) in self.time_dn_pairs:
            start_dt = CHINA_TZ.localize(datetime.datetime.combine(today,start)) + datetime.timedelta(days=dn1)
            stop_dt = CHINA_TZ.localize(datetime.datetime.combine(today,stop)) + datetime.timedelta(days=dn2)
            time_delta = stop_dt - start_dt
            TTPD = TTPD + time_delta
        return int(TTPD.seconds/60)

    def get_trade_time_inday(self,dt:datetime,unit:Timeunit=Timeunit.MINUTE):
        """ 
        计算dt在交易日内的分钟数 
        unit: '1s':second;'1m':minute;'1h';1h
        """
        TTID = datetime.timedelta(0,0,0)

        day,trade_times = self.get_trade_datetimes(dt,allday=True)
        if not trade_times:
            return None

        for start,stop in trade_times:
            if dt > stop:
                time_delta = stop - start
                TTID += time_delta
            elif dt > start:
                time_delta = dt - start
                TTID += time_delta     
                break
            else:
                break          

        if unit == Timeunit.SECOND:
            return TTID.seconds
        elif unit == Timeunit.MINUTE:
            return int(TTID.seconds/60) 
        elif unit == Timeunit.HOUR:
            return int(TTID.seconds/3600) 
        else:
            return TTID

    def get_day_tradetimes(self,dt:datetime):
        """ 
        得到合约日盘的交易时间段 
        """
        index,trade_times = self.get_trade_datetimes(dt,allday=True)
        trade_times1 = []
        if trade_times:
            for start_dt,stop_dt in trade_times:
                if start_dt.time() < datetime.time(18,0,0):
                    trade_times1.append((start_dt,stop_dt))
            return index,trade_times1
        return (index,trade_times1)

    def get_night_tradetimes(self,dt:datetime):
        """ 
        得到合约夜盘的交易时间段 
        """
        index,trade_times = self.get_trade_datetimes(dt,allday=True)
        trade_times1 = []
        if trade_times:
            for start_dt,stop_dt in trade_times:
                if start_dt.time() > datetime.time(18,0,0):
                    trade_times1.append((start_dt,stop_dt))
            return index,trade_times1
        return (index,trade_times1)

    def convet_to_datetime(self,day:int,minutes:int):
        """ 
        计算minutes在第day交易日内的datetime形式的时间 
        """
        date = self.trade_index_date.get(day,None)
        if date is None:
            return None
        idx,trade_times = self.trade_date_index.get(date,(None,[]))
        if not trade_times:     # 不一定必要
            return None
        for (start,stop) in trade_times:
            timedelta = stop - start 
            if minutes < int(timedelta.seconds/60):
                return start + datetime.timedelta(minutes=minutes)
            else:
                minutes -= int(timedelta.seconds/60)
        return None

    def get_bar_window(self,dt:datetime,window:int,interval:Interval=Interval.MINUTE):
        """ 
        计算dt所在K线的起止时间 
        """
        bar_windows = (None,None)

        day,trade_times = self.get_trade_datetimes(dt,allday=True)
        if not trade_times:
            # print(f"day={day} trade_times={trade_times}")
            return bar_windows

        # 求每个交易日的交易时间分钟数
        TTPD = self.get_trade_time_perday()

        # 求dt在交易日内的分钟数
        TTID = self.get_trade_time_inday(dt,unit=Timeunit.MINUTE)

        # 得到dt时刻K线的起止时间 
        total_minites = day*TTPD + TTID

        # 计算K线宽度(分钟数)
        if interval == Interval.MINUTE:
            bar_width = window
        elif interval == Interval.HOUR:
            bar_width = 60*window
        elif interval == Interval.DAILY:
            bar_width = TTPD*window
        elif interval == Interval.WEEKLY:
            bar_width = TTPD*window*5
        else:
            return bar_windows

        # 求K线的开始时间的和结束的分钟形式
        start_m = int(total_minites/bar_width)*bar_width
        stop_m = start_m + bar_width

        # 计算K开始时间的datetime形式
        start_d = int(start_m / TTPD)
        minites = start_m % TTPD
        start_dt = self.convet_to_datetime(start_d,minites)
        # print(f"start_d={start_d} minites={minites}---->{start_dt}")

        # 计算K结束时间的datetime形式
        stop_d = int(stop_m / TTPD)
        minites = stop_m % TTPD
        stop_dt = self.convet_to_datetime(stop_d,minites)
        # print(f"stop_d={stop_d} minites={minites}---->{stop_dt}")

        return start_dt,stop_dt

    def get_date_start_stop(self,dt:datetime):
        """
        获得dt所在交易日的开始和停止时间
        """
        index,trade_times = self.get_trade_datetimes(dt,allday=True)
        if trade_times:
            valid_dt = False
            for t1,t2 in trade_times:
                if t1 < dt and dt < t2:
                    valid_dt = True
                    break
            if valid_dt:
                start_dt = trade_times[0][0]
                stop_dt = trade_times[-1][1]
                return True,(start_dt,stop_dt)
        return False,(None,None)

    def get_day_start_stop(self,dt:datetime):
        """
        获得dt所在交易日日盘的开始和停止时间
        """
        index,trade_times = self.get_day_tradetimes(dt)
        if trade_times:
            valid_dt = False
            for t1,t2 in trade_times:
                if t1 <= dt and dt < t2:
                    valid_dt = True
                    break
            if valid_dt:
                start_dt = trade_times[0][0]
                stop_dt = trade_times[-1][1]
                return True,(start_dt,stop_dt)
        return False,(None,None)

    def get_night_start_stop(self,dt:datetime):
        """
        获得dt所在交易日夜盘的开始和停止时间
        """
        index,trade_times = self.get_night_tradetimes(dt)
        if trade_times:
            valid_dt = False
            for t1,t2 in trade_times:
                if t1 <= dt and dt < t2:
                    valid_dt = True
                    break
            if valid_dt:
                start_dt = trade_times[0][0]
                stop_dt = trade_times[-1][1]
                return True,(start_dt,stop_dt)
        return False,(None,None)

    def get_next_holiday(self,dt:datetime):
        """ 
        计算下一个节假日的起止日期
        注意:dt必须为交易时间,否则返回为(0,(None,None))
        """
        days,holday_start,holday_stop = 0,None,None
        index,trade_times = self.get_trade_datetimes(dt)
        if trade_times:
            # dt在交易时间段内
            day_count = len(self.trade_index_date)
            for idx in range(index,day_count-1):
                # 寻找间隔超过一天的前后两个交易日
                date1 = self.trade_index_date[idx]
                date2 = self.trade_index_date[idx+1]
                date_diff = date2 - date1
                if date_diff > datetime.timedelta(days=1):
                    # 找到了间隔超过一天的前后两个交易日
                    days = date_diff.days - 1
                    # 前1日加一天为节假日开始
                    holday_start = date1 + datetime.timedelta(days=1)
                    # 后1日减一天为节假日结束
                    holday_stop = date2 + datetime.timedelta(days=-1)
                    break

        return days,(holday_start,holday_stop)

    def to_holiday_time(self,dt:datetime):
        """
        计算交易时间距离节假日的时间。
        无夜盘合约 : 到节假日前一交易日的日盘收盘时间
        有夜盘合约 : 到节假日后一交易日的夜盘收盘时间
        注意:dt必须为交易时间,否则返回为None
              返回值:
        """
        index,trade_times = self.get_trade_datetimes(dt,True)
        if trade_times:
            # dt在交易时间段内
            day_count = len(self.trade_index_date)
            for idx in range(index,day_count-1):
                # 寻找间隔超过一天的前后两个交易日
                date1 = self.trade_index_date[idx]
                date2 = self.trade_index_date[idx+1]
                date_diff = date2 - date1
                if date_diff > datetime.timedelta(days=1):
                    if not self.has_night_trading:
                        days,trade_times = self.get_date_tradetimes(date1)
                        stop_dt = trade_times[-1][1]
                        # print(f"{stop_dt} ")
                    else:
                        days,trade_times = self.get_date_tradetimes(date2)
                        dt1 = trade_times[0][0] 
                        has,(start_dt,stop_dt) = self.get_night_start_stop(dt1)
                        # print(f"dt1={dt1},{(start_dt,stop_dt)} ")

                    # print(f"has_night_trading ={self.has_night_trading}, stop_dt={stop_dt}")  
                    time_diff = stop_dt - dt
                    return time_diff
        return None


if __name__ == "__main__":

    def test1():
        # vt_symbols = ["rb2010.SHFE","ag2012.SHFE","i2010.DCE"]
        vt_symbols = ["ag2012.SHFE"]
        date0 = datetime.date(2020,8,31)
        dt0 = CHINA_TZ.localize(datetime.datetime(2020,8,31,9,20,15))
        for vt_symbol in vt_symbols:
            symbol,exchange = extract_vt_symbol(vt_symbol)
            th = TradeHours(symbol)
            # trade_hours = th.get_date_tradetimes(date0)
            # print(f"\n{vt_symbol} {date0} trade_hours={trade_hours}")

            days,trade_hours = th.get_trade_datetimes(dt0,allday=True)

            print(f"\n{vt_symbol} {dt0} days:{days} trade_hours={trade_hours}")

            if trade_hours:
                day_start = trade_hours[0][0]
                day_end = trade_hours[-1][1]
                print(f"day_start={day_start} day_end={day_end}")
                exit_time = day_end + datetime.timedelta(minutes=-5)
                print(f"exit_time={exit_time}")

            dt1 = CHINA_TZ.localize(datetime.datetime(2020,8,31,9,20,15))
            dt2 = CHINA_TZ.localize(datetime.datetime(2020,9,1,1,1,15))

            for dt in [dt1,dt2]:
                in_trade,(start,stop) = th.get_date_start_stop(dt)
                if (in_trade):
                    print(f"\n{vt_symbol} 时间 {dt} 交易日起止:{start,stop}")
                else:
                    print(f"\n{vt_symbol} 时间 {dt} 非交易时间")

                in_day,(start,stop) = th.get_day_start_stop(dt)
                if (in_day):
                    print(f"\n{vt_symbol} 时间 {dt} 日盘起止:{start,stop}")
                else:
                    print(f"\n{vt_symbol} 时间 {dt} 非日盘时间")

                in_night,(start,stop) = th.get_night_start_stop(dt)
                if in_night:
                    print(f"\n{vt_symbol} 时间 {dt} 夜盘起止:{start,stop}")
                else:
                    print(f"\n{vt_symbol} 时间 {dt} 非夜盘时间")

    def test2():
        vt_symbols = ["ag2012.SHFE"]
        date0 = datetime.date(2020,8,31)
        dt0 = CHINA_TZ.localize(datetime.datetime(2019,12,10,9,20,15))

        for vt_symbol in vt_symbols:      
            symbol,exchange = extract_vt_symbol(vt_symbol)
            th = TradeHours(symbol)

            for i in range(365):
                td = th.listed_date + datetime.timedelta(days=i)
                dt = CHINA_TZ.localize(
                        datetime.datetime.combine(td,datetime.time(9,20,15)))
                days,(date1,date2) = th.get_next_holiday(dt)
                print(f"dt={dt},days={days},holiday={date1,date2}")            



    def test3():
        """
        TradeHour.to_holiday_time()验证
        """
        vt_symbol = "ag2012.SHFE"
        date0 = datetime.date(2020,8,29)
        dt0 = CHINA_TZ.localize(datetime.datetime(2020,8,28,9,20,15))

        symbol,exchange = extract_vt_symbol(vt_symbol)
        th = TradeHours(symbol)

        for i in range(1200):
            dt = dt0 + datetime.timedelta(minutes=i)
            time_diff = th.to_holiday_time(dt)
            if time_diff:
                print(f"dt={dt},time_diff,time_diff={time_diff}")            


    # 开始测试
    rq.init('xxxxxx','******',("rqdatad-pro.ricequant.com",16011))

    # test1()
    # test2()
    test3()

启动时选择algo应用,遇到下面的错误(首次尝试运行algo应用)

Traceback (most recent call last):
  File "D:\ProgramFiles\VnStudio\lib\site-packages\vnstation\cli.py", line 90, in run_trader
    module = importlib.import_module(d["module"])
  File "D:\ProgramFiles\VnStudio\lib\importlib\__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1006, in _gcd_import
  File "<frozen importlib._bootstrap>", line 983, in _find_and_load
  File "<frozen importlib._bootstrap>", line 967, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 677, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 728, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "D:\ProgramFiles\VnStudio\lib\site-packages\vnpy\app\algo_trading\__init__.py", line 5, in <module>
    from .engine import AlgoEngine, APP_NAME
  File "D:\ProgramFiles\VnStudio\lib\site-packages\vnpy\app\algo_trading\engine.py", line 17, in <module>
    from .genus import GenusClient
  File "D:\ProgramFiles\VnStudio\lib\site-packages\vnpy\app\algo_trading\genus.py", line 6, in <module>
    import quickfix as fix
ModuleNotFoundError: No module named 'quickfix'

从错误提示看是缺少了quickfix模块,那就安装
于是进入VN Staudio Prompt窗口下输入:

pip install quickfix

安装quickfix模块,提示趋势Ms Visual C++ V14.0 build tools

那就先安装Ms Visual C++ V14.0 build tools,
搜索到Ms Visual C++ V14.0 build tools,
也装上了,
提示重新启动,
那就重新启动

再次安装quickfix

命令是:

pip install quickfix

出错啦:

D:\ProgramFiles\VnStudio>pip install quickfix
Collecting quickfix
  Using cached quickfix-1.15.1.tar.gz (1.5 MB)
Using legacy 'setup.py install' for quickfix, since package 'wheel' is not installed.
Installing collected packages: quickfix
    Running setup.py install for quickfix ... error
    ERROR: Command errored out with exit status 1:
     command: 'd:\programfiles\vnstudio\python.exe' -u -c 'import sys, setuptools, tokenize; sys.argv[0] = '"'"'C:\\Users\\hxxja\\AppData\\Local\\Temp\\pip-install-1sa6_klk\\quickfix\\setup.py'"'"'; __file__='"'"'C:\\Users\\hxxja\\AppData\\Local\\Temp\\pip-install-1sa6_klk\\quickfix\\setup.py'"'"';f=getattr(tokenize, '"'"'open'"'"', open)(__file__);code=f.read().replace('"'"'\r\n'"'"', '"'"'\n'"'"');f.close();exec(compile(code, __file__, '"'"'exec'"'"'))' install --record 'C:\Users\hxxja\AppData\Local\Temp\pip-record-qavjungc\install-record.txt' --single-version-externally-managed --compile --install-headers 'd:\programfiles\vnstudio\Include\quickfix'
         cwd: C:\Users\hxxja\AppData\Local\Temp\pip-install-1sa6_klk\quickfix\
    Complete output (38 lines):
    running install
    running build
    running build_py
    creating build
    creating build\lib.win-amd64-3.7
    copying quickfix.py -> build\lib.win-amd64-3.7
    copying quickfixt11.py -> build\lib.win-amd64-3.7
    copying quickfix40.py -> build\lib.win-amd64-3.7
    copying quickfix41.py -> build\lib.win-amd64-3.7
    copying quickfix42.py -> build\lib.win-amd64-3.7
    copying quickfix43.py -> build\lib.win-amd64-3.7
    copying quickfix44.py -> build\lib.win-amd64-3.7
    copying quickfix50.py -> build\lib.win-amd64-3.7
    copying quickfix50sp1.py -> build\lib.win-amd64-3.7
    copying quickfix50sp2.py -> build\lib.win-amd64-3.7
    running build_ext
    Testing for std::tr1::shared_ptr...
    C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\BIN\x86_amd64\cl.exe /c /nologo /Ox /W3 /GL /DNDEBUG /MD -DPYTHON_MAJOR_VERSION=3 -IC++ -Id:\programfiles\vnstudio\include -Id:\programfiles\vnstudio\include "-IC:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\INCLUDE" "-IC:\Program Files (x86)\Windows Kits\10\include\10.0.10240.0\ucrt" "-IC:\Program Files (x86)\Windows Kits\8.1\include\shared" "-IC:\Program Files (x86)\Windows Kits\8.1\include\um" "-IC:\Program Files (x86)\Windows Kits\8.1\include\winrt" "-IC:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\INCLUDE" "-IC:\Program Files (x86)\Windows Kits\10\include\10.0.10240.0\ucrt" "-IC:\Program Files (x86)\Windows Kits\8.1\include\shared" "-IC:\Program Files (x86)\Windows Kits\8.1\include\um" "-IC:\Program Files (x86)\Windows Kits\8.1\include\winrt" /EHsc /Tptest_std_tr1_shared_ptr.cpp /Fotest_std_tr1_shared_ptr.obj
    test_std_tr1_shared_ptr.cpp
    test_std_tr1_shared_ptr.cpp(1): fatal error C1083: Cannot open include file: 'tr1/memory': No such file or directory
     ...not found
    Testing for std::shared_ptr...
    C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\BIN\x86_amd64\cl.exe -std=c++0x /c /nologo /Ox /W3 /GL /DNDEBUG /MD -DPYTHON_MAJOR_VERSION=3 -IC++ -Id:\programfiles\vnstudio\include -Id:\programfiles\vnstudio\include "-IC:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\INCLUDE" "-IC:\Program Files (x86)\Windows Kits\10\include\10.0.10240.0\ucrt" "-IC:\Program Files (x86)\Windows Kits\8.1\include\shared" "-IC:\Program Files (x86)\Windows Kits\8.1\include\um" "-IC:\Program Files (x86)\Windows Kits\8.1\include\winrt" "-IC:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\INCLUDE" "-IC:\Program Files (x86)\Windows Kits\10\include\10.0.10240.0\ucrt" "-IC:\Program Files (x86)\Windows Kits\8.1\include\shared" "-IC:\Program Files (x86)\Windows Kits\8.1\include\um" "-IC:\Program Files (x86)\Windows Kits\8.1\include\winrt" /EHsc /Tptest_std_shared_ptr.cpp /Fotest_std_shared_ptr.obj
    cl : Command line warning D9002 : ignoring unknown option '-std=c++0x'
    test_std_shared_ptr.cpp
    ...found
    Testing for std::unique_ptr...
    C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\BIN\x86_amd64\cl.exe -std=c++0x /c /nologo /Ox /W3 /GL /DNDEBUG /MD -DPYTHON_MAJOR_VERSION=3 -DHAVE_STD_SHARED_PTR -IC++ -Id:\programfiles\vnstudio\include -Id:\programfiles\vnstudio\include "-IC:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\INCLUDE" "-IC:\Program Files (x86)\Windows Kits\10\include\10.0.10240.0\ucrt" "-IC:\Program Files (x86)\Windows Kits\8.1\include\shared" "-IC:\Program Files (x86)\Windows Kits\8.1\include\um" "-IC:\Program Files (x86)\Windows Kits\8.1\include\winrt" "-IC:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\INCLUDE" "-IC:\Program Files (x86)\Windows Kits\10\include\10.0.10240.0\ucrt" "-IC:\Program Files (x86)\Windows Kits\8.1\include\shared" "-IC:\Program Files (x86)\Windows Kits\8.1\include\um" "-IC:\Program Files (x86)\Windows Kits\8.1\include\winrt" /EHsc /Tptest_std_unique_ptr.cpp /Fotest_std_unique_ptr.obj
    cl : Command line warning D9002 : ignoring unknown option '-std=c++0x'
    test_std_unique_ptr.cpp
    ...found
    building '_quickfix' extension
    creating build\temp.win-amd64-3.7
    creating build\temp.win-amd64-3.7\Release
    creating build\temp.win-amd64-3.7\Release\C++
    C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\BIN\x86_amd64\cl.exe /c /nologo /Ox /W3 /GL /DNDEBUG /MD -DPYTHON_MAJOR_VERSION=3 -DHAVE_STD_SHARED_PTR -DHAVE_STD_UNIQUE_PTR -IC++ -Id:\programfiles\vnstudio\include -Id:\programfiles\vnstudio\include "-IC:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\INCLUDE" "-IC:\Program Files (x86)\Windows Kits\10\include\10.0.10240.0\ucrt" "-IC:\Program Files (x86)\Windows Kits\8.1\include\shared" "-IC:\Program Files (x86)\Windows Kits\8.1\include\um" "-IC:\Program Files (x86)\Windows Kits\8.1\include\winrt" "-IC:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\INCLUDE" "-IC:\Program Files (x86)\Windows Kits\10\include\10.0.10240.0\ucrt" "-IC:\Program Files (x86)\Windows Kits\8.1\include\shared" "-IC:\Program Files (x86)\Windows Kits\8.1\include\um" "-IC:\Program Files (x86)\Windows Kits\8.1\include\winrt" /EHsc /TpC++\Acceptor.cpp /Fobuild\temp.win-amd64-3.7\Release\C++\Acceptor.obj -std=c++0x -Wno-deprecated -Wno-unused-variable -Wno-deprecated-declarations -Wno-maybe-uninitialized
    cl : Command line error D8021 : invalid numeric argument '/Wno-deprecated'
    error: command 'C:\\Program Files (x86)\\Microsoft Visual Studio 14.0\\VC\\BIN\\x86_amd64\\cl.exe' failed with exit status 2
    ----------------------------------------
ERROR: Command errored out with exit status 1: 'd:\programfiles\vnstudio\python.exe' -u -c 'import sys, setuptools, tokenize; sys.argv[0] = '"'"'C:\\Users\\hxxja\\AppData\\Local\\Temp\\pip-install-1sa6_klk\\quickfix\\setup.py'"'"'; __file__='"'"'C:\\Users\\hxxja\\AppData\\Local\\Temp\\pip-install-1sa6_klk\\quickfix\\setup.py'"'"';f=getattr(tokenize, '"'"'open'"'"', open)(__file__);code=f.read().replace('"'"'\r\n'"'"', '"'"'\n'"'"');f.close();exec(compile(code, __file__, '"'"'exec'"'"'))' install --record 'C:\Users\hxxja\AppData\Local\Temp\pip-record-qavjungc\install-record.txt' --single-version-externally-managed --compile --install-headers 'd:\programfiles\vnstudio\Include\quickfix' Check the logs for full command output.

被吓到了,不就是安装个algo需要的模块吗,出这么多错误!!!

缺啥给啥,要啥补啥,不待这样折磨人的!

那位大神遇到了这种问题,分享下解决问题的经验吧,先叩谢!!!

1. 现象见图中描述

description

1.1 这是接口的错误?

在cta_gateway.py中send_order()函数中,找到出错语句前增加下面的调试打印:

        self.reqid += 1
        print(f"ctp_req={ctp_req}")    # 调试打印
        self.reqOrderInsert(ctp_req, self.reqid)

得到ctp_req的内容如下:

ctp_req={
    'InstrumentID': 'ag2012', 
    'ExchangeID': 'SHFE', 
    'LimitPrice': 6093.0, 
    'VolumeTotalOriginal': 8, 
    'OrderPriceType': '', 
    'Direction': '1', 
    'CombOffsetFlag': '3', 
    'OrderRef': '1', 
    'InvestorID': '147102', 
    'UserID': '147102', 
    'BrokerID': '9999', 
    'CombHedgeFlag': '1', 
    'ContingentCondition': '1', 
    'ForceCloseReason': '0', 
    'IsAutoSuspend': 0, 
    'TimeCondition': '3', 
    'VolumeCondition': '1', 
    'MinVolume': 1
    }

这里面的内容只有最为可疑:

'OrderPriceType': '',

2. 为什么会出错?

2.1 停止单分为两种:

  • 服务器停止单:需要交易所支持,直接可以发送服务器处理
  • 本地停止单:交易所不支持,由本地应用来维护

2.2 CTA的网关目前只处理两类委托请求:

ORDERTYPE_VT2CTP = {
    OrderType.LIMIT: THOST_FTDC_OPT_LimitPrice,   # 限价类型
    OrderType.MARKET: THOST_FTDC_OPT_AnyPrice # 市价类型
}
ORDERTYPE_CTP2VT = {v: k for k, v in ORDERTYPE_VT2CTP.items()}

2.3 用户的CTA策略的停止单是有CtaEngine维护的

用户的CTA策略发送委托停止单的过程:

    1. 调用CtaTemplate的send_order()
    1. 再调用CtaEngine引擎的send_order()
    1. CtaEngine引擎区分委托请求时 限价单还是停止单,
    1. CtaEngine引擎通过过请求中合约的contract,得知是否支持服务器停止单,如果支持直接调用main_engine中ctp_gateway发送到服务器,否则有引擎保存到本地进行维护
    1. CtaEngine引擎中的本地停止单被触发时,CtaEngine引擎把当触发停止单转化未涨停板或跌停板限价单,调用main_engine中ctp_gateway发送到服务器
    1. ctp_gateway对vnpy委托请求OrderRequest中的OrderType进行映射,得到接口识别的请求类型OrderPriceType,OrderPriceType目前只接受两个类型:THOST_FTDC_OPT_LimitPrice和THOST_FTDC_OPT_AnyPrice。
    1. 转化后委托请求ctp_req其实是个字典,然后通过交易接口TdApi发送给交易服务器

3. 假如你从Main_Window(主界面)直接发送停止单,问题就来 !

3.1 Main_Window直接发送停止单过程:

  • 1 系统是直接调用ctp_gateway,它不识别服务器是否支持
  • 2 系统它不会把该停止单发送到CtaEngine引擎,因为你可以压根就没有启动CtaEngine引擎模块,怎么调用?
  • 3 委托请求OrderRequest中的OrderType='STOP',在ORDERTYPE_VT2CTP字典中就没有键值,产生出来的接口请求类型OrderPriceType=''
  • 4 于是就出错啦!!!

3.2 结论:

目前在主界面中是不可以发送任何停止单的!!!
目前在主界面中是不可以发送任何停止单的!!!
目前在主界面中是不可以发送任何停止单的!!!

重要的话说三遍

4. 发送委托停止单出错的修改建议:

  • 1 类型映射错误,就算是服务器停止单支持,我敢断言也会出错,因为OrderPriceType=‘’ 肯定是不可以被接口接受的,这个需要改!
  • 2 主界面中发送的停止单,没有做服务器是支持的判断,如果不支持也的话也无人维护。那么是否可以把下级应用cta_engine的对本地停止单的维护,上提到main_engine中来执行?
  • 3 CTA策略都可以发委托单,为什么手工却不让做呢 ?这么好的功能,应该人工也享受享受!

已经分析过了,那么是否可以改进呢?

1. 等自然时长机制的K线生成器说明

交易日内等自然时长K线生成器,可以实现周以下的周期单位的时间就间隔:
x=window,interval的取:
Interval.MINUTE :x分钟宽度的K线,对齐交易日开始时间,不可以超过日交易时长
Interval.HOUR   : x小时宽度的K线,对齐交易日开始时间,不可以超过日交易时长
Interval.DAILY  : x日宽度的K线,对齐上市交易日开始时间开始的整x日,x可以任意值
Interval.WEEKLY  : x周宽度的K线,对齐上市交易日开始时间整x周,x可以任意值

再策略中的应用举例:

  • 等自然时长机制的30分钟K线创建:
    self.bg = ENTBarGenerator(self.on_bar,30,self.on_30m_bar,interval=Interval.MINUTE,self.vt_symbol)
  • 等自然时长机制的2小时K线创建:
    self.bg = ENTBarGenerator(self.on_bar,120,self.on_2h_bar,interval=Interval.MINUTE,self.vt_symbol)
    或者:
    self.bg = ENTBarGenerator(self.on_bar,2,self.on_2h_bar,interval=Interval.HOUR,self.vt_symbol)
  • 等自然时长机制的1日K线创建:
    self.bg = ENTBarGenerator(self.on_bar,1,self.on_1d_bar,interval=Interval.DAILY,self.vt_symbol)
  • 等自然时长机制的1周K线创建:
    self.bg = ENTBarGenerator(self.on_bar,5,self.on_1w_bar,interval=Interval.DAILY,self.vt_symbol)
    或者:
    self.bg = ENTBarGenerator(self.on_bar,1,self.on_1w_bar,interval=Interval.WEEKLY,self.vt_symbol)

2. 先实现等自然时长机制的K线生成器

保存文件:vnpy\usertools\equal_nature_time.py
内容如下:

"""
实现一个等自然时长机制的K线生成器

作者:hxxjava 
日期:2020-9-2
"""

from typing import Callable,List,Dict, Tuple, Union
from vnpy.app.cta_strategy import BarGenerator
from vnpy.trader.constant import Interval
from vnpy.trader.utility import extract_vt_symbol
from vnpy.trader.object import BarData

import pytz
CHINA_TZ = pytz.timezone("Asia/Shanghai")

from vnpy.usertools.trade_hour import (
    TradeHours,
    Timeunit,
    get_listed_date,
    get_de_listed_date,
)

import datetime 
import rqdatac as rq


class MyError(Exception):
    def __init__(self,ErrorInfo):
        super().__init__(self) #初始化父类
        self.errorinfo=ErrorInfo
    def __str__(self):
        return self.errorinfo       

class ENTBarGenerator(BarGenerator):
    """ 
    交易日内等自然时长K线生成器,可以实现周以下的周期单位的时间就间隔:
    x=window,interval的取:
    Interval.MINUTE :x分钟宽度的K线,对齐交易日开始时间,不可以超过日交易时长
    Interval.HOUR   : x小时宽度的K线,对齐交易日开始时间,不可以超过日交易时长
    Interval.DAILY  : x日宽度的K线,对齐上市交易日开始时间开始的整x日,x可以任意值
    Interval.WEEKLY  : x周宽度的K线,对齐上市交易日开始时间整x周,x可以任意值

    ENT:equal nature time ———— 等自然时长的缩写
    """ 
    def __init__(
        self,
        on_bar: Callable,
        window: int = 0,
        on_window_bar: Callable = None,
        interval: Interval = Interval.MINUTE,
        vt_symbol:str=''
    ):
        super().__init__(on_bar,window,on_window_bar,interval)

        symbol,exchange = extract_vt_symbol(vt_symbol)
        self.trade_hours = TradeHours(symbol.upper())

        self.trade_start,self.trade_stop = None,None

        self.bar_width = self.get_bar_witdh()
        self.bar_index = None

        if self.interval in [Interval.DAILY,Interval.WEEKLY]:
            self.kx_start,self.kx_stop = (None,None)

    def get_bar_witdh(self):
        """
        求window_bar的宽度
        """
        # 求每日的交易时长
        TTPD = self.trade_hours.get_trade_time_perday()

        try:
            if self.interval == Interval.MINUTE:
                # 以分钟作为单位
                bar_width = self.window
                if bar_width>TTPD:
                    raise MyError(f'window_bar宽度不可以大于1日的交易总时长')

            elif self.interval == Interval.HOUR:
                # 以小时作为单位
                bar_width = self.window*60
                if bar_width>TTPD:
                    raise MyError(f'window_bar宽度不可以大于1日的交易总时长')

            elif self.interval == Interval.DAILY:
                # 以日作为单位
                bar_width = self.window * TTPD

            elif self.interval == Interval.WEEKLY:
                # 以周作为单位
                bar_width = self.window * TTPD * 5

            else:
                raise MyError(f'不可以使用{self.interval}作为interval参数')

            return bar_width

        except MyError as e:
            print(e)


    def get_bar_index(self,bar:BarData):
        """
        计算当前分钟bar所属window_bar的日内索引
        """
        time_diff = bar.datetime - self.trade_start
        days = time_diff.days 
        sec = time_diff.seconds
        us = time_diff.microseconds
        minutes = (days*24*3600 +sec+us*0.000001)/60.0 
        index = int(minutes/self.bar_width)
        return index 

    def update_bar(self,bar:BarData) -> None:
        if self.interval in [Interval.MINUTE,Interval.HOUR]:
            self.update_bar1(bar)
        elif self.interval in [Interval.DAILY,Interval.WEEKLY]:
            self.update_bar2(bar)

    def update_bar1(self,bar:BarData) -> None:
        """
        Update 1 minute bar into generator
        """

        # 如果bar的时间戳笔windows的时间戳还早,丢弃bar
        if self.window_bar and bar.datetime < self.window_bar.datetime:
            return

        in_trade,(trade_start,trade_stop) = self.trade_hours.get_date_start_stop(bar.datetime)
        if not in_trade:
            # 无效K线,不可以处理
            # print(f"bar.datetime= {bar.datetime} 无效K线,不可以处理")
            return

        if (self.trade_start,self.trade_stop) == (None,None):
            # 计算当日的开始和停止交易时间
            self.trade_start,self.trade_stop = trade_start,trade_stop
        elif trade_start != self.trade_start:
            # 判断是否跨日,更新当日的开始和停止交易时间
            self.trade_start,self.trade_stop = trade_start,trade_stop

        # print(f"!1 {self.trade_start,self.trade_stop}")

        # 计算当前window_bar的日内索引
        bar_index = self.get_bar_index(bar)

        if self.bar_index is None or bar_index != self.bar_index:
            # 产生新window_barK线
            self.bar_index = bar_index

            if self.window_bar:
                # 推送已经走完的window_bar
                self.on_window_bar(self.window_bar)
                self.window_bar = None

            # 计算K线的交易起止时间
            bar_datetime = self.trade_start + datetime.timedelta(minutes=self.bar_index*self.bar_width)

            # 生成新的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,
            )         

        # 更新window_bar的high和low
        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)

        # 更新 close price/volume到window bar
        self.window_bar.close_price = bar.close_price
        self.window_bar.volume += int(bar.volume)
        self.window_bar.open_interest = bar.open_interest 

    def update_bar2(self,bar:BarData) -> None:
        """
        Update 1 minute bar into generator
        """

        # 如果bar的时间戳笔windows的时间戳还早,丢弃bar
        if self.window_bar and bar.datetime < self.window_bar.datetime:
            return

        if (self.kx_start,self.kx_stop) == (None,None):
            self.kx_start,self.kx_stop = self.trade_hours.get_bar_window(bar.datetime,self.window,self.interval)
            if (self.kx_start,self.kx_stop) == (None,None):
                return

        # If not inited, creaate window bar object
        if (not self.window_bar):
            # 获得K线的交易起止时间
            self.window_bar = BarData(
                symbol=bar.symbol,
                exchange=bar.exchange,
                datetime=self.kx_start,
                gateway_name=bar.gateway_name,
                open_price=bar.open_price,
                high_price=bar.high_price,
                low_price=bar.low_price,
            )         

        elif self.kx_start <= bar.datetime and bar.datetime < self.kx_stop:
            # 1分钟K线属于当前K线
            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)

        elif bar.datetime >= self.kx_stop:       # Check if window bar completed
            self.on_window_bar(self.window_bar)
            self.window_bar = None

            self.kx_start,self.kx_stop = self.trade_hours.get_bar_window(bar.datetime,self.window,self.interval)
            if (self.kx_start,self.kx_stop) == (None,None): 
                # 不在交易时段
                return

            self.window_bar = BarData(
                symbol=bar.symbol,
                exchange=bar.exchange,
                datetime=self.kx_start,
                gateway_name=bar.gateway_name,
                open_price=bar.open_price,
                high_price=bar.high_price,
                low_price=bar.low_price,
            )       

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



# 下面是ENTBarGenerator类的测试代码
def test():
    from vnpy.trader.constant import Exchange
    import pytz
    CHINA_TZ = pytz.timezone("Asia/Shanghai")

    def on_bar(bar: BarData):
        pass

    def on_xbar_bar(bar: BarData):
        print(bar)

    bar_gen = ENTBarGenerator(on_bar,30,on_xbar_bar,Interval.MINUTE,vt_symbol='rb2010.SHFE')
    dt0 = CHINA_TZ.localize(datetime.datetime(2020,8,31,9,20,1))

    for i in range(60*24):
        # print(f"{[dt0 + datetime.timedelta(minutes=i)]}")
        bar = BarData(
            gateway_name = 'CTP', 
            symbol = 'rb2010', 
            exchange = Exchange.SHFE, 
            datetime = dt0 + datetime.timedelta(minutes=i),
            interval=Interval.MINUTE,
            volume=2,
            open_price=3560.0,
            high_price= 3565.0,
            close_price=3558.0,
            low_price=3555.0,
            open_interest=10,
        )

        bar_gen.update_bar(bar)

if __name__ == "__main__":
    # RQDatac初始化
    rq.init('xxxxxx','******',("rqdatad-pro.ricequant.com",16011))

    test()

1. 不是所有期货都是一日7根小时线

1)self.bg = BarGenerator(slef.on_bar,7,self.on_day_bar,interval=Interval.Hour)生成日线对部分期货合约适用,如RB,AP,I之类的,对另外的却是错误的,因为:
ag(白银)11根:
21:00~22:00 ,22:00~23:00 ,23:00~00:00,00:00~01:00,01:00~02:00, 02:00~02:30,9:00~10:00 , 10:00~11:00, 11:00~11:30, 13:30~14:00 , 14:00~15:00
IF(股指)5根:
9:30~10:00 , 10:00~11:00, 11:00~11:30, 13:30~14:00 , 14:00~15:00

2. 以谁为标准判断对错?

你是如何确定你生成K线open,high,low,close和实际值不一致?和谁比较?我猜你是用第三方软件来比较的吧!
那你知道你使用的第三方软件它生成的日K线规则是什么?大智慧、通达信、博弈、文华财经?它们本身的日K线的产生规则都不完全相同,有的是区分日盘和夜盘的,有的却不区分,有的是等自然时长,有的等交易时长。
不同的产生机制,产生不同K线,不可以说谁是对的,谁是错误的。

3. 数据采集不同会造成open,high,low,close不一样

1)大智慧、通达信、博弈、文华财经和米筐都是从交易所采集数据(tick),然后自己合成各自K线。
2)它们可能使用了交易所的不同的数据采集通道(这些通道都是真实有效的)。
3)这些不同的数据采集通道,交易所保证真实性,不保证一致性,也就是说可能是不一样的
4)你可以去比较大智慧、通达信、文华财经,选择同一个合约的同一个周期,比较一下看看,都可能是不一样的

因为测不准原理,世界本来就是没有绝对的准确,只有相对准确。你永远无法知道1米的绝对长度是多少!你不可以说哪个数据采集是正确的,哪个是错误的。

4 这样的K线比较才有意义:

  • 相同数据采集通道:这个你通常是不知道的,问问你数据提供商(也许他们也不确定知道,除非你问对了人——他特别清楚接口底层)。但是这个因素造成的数据不同概率比较低,但确实存在。
  • 相同K线生成机制:如:都是自然时间机制,这通常可以通过第三方软件的设置中自己选择(如果可以选择的话,如文华赢顺客户端就是等自然时长机制,文华赢智客户端就可以选择等自然时长机制、等交易时长机制,还有等交易时长机制改进型[分别从夜盘、日盘的开始时间开始划分K线]),然后你在vnpy中确定你的K线生成机制,能保证二者相同。更多的知识参见 K线种类总结
  • 相同的合约:这个最容易保证了(哈哈)
  • 相同的时间段:这个也容易保证了(哈哈)

准备好这些,如果你还是发现K线不一样,你可以怀疑的元素:

  • 1 数据接口丢了
  • 2 数据供应商数据转发出错了,如果是付费的你可以去理论理论,如果是免费的就无人可问啦

1. 什么是策略账户?

策略账户有点类似文华赢智和库安软件中的模组账户的概念,但它根植vnpy的系统架构,完全用python写成。

  1. 策略账户是一个虚拟的账户。它可以为每个策略运行实例分配虚拟资金,如果有需要你还可以进行入金和出金操作,当然这也是虚拟的资金操作。
  2. 用户策略可以从创建增加策略账户,可以从策略账户中获取当前可用资金,以控制策略的交易规模。
  3. 策略账户在后台跟踪记录用户策略交易活动,从而用户策略可以查询历史交易和当前交易的盈亏状况。
  4. 策略账户还可以设定风控水平报警,为用户策略交易提供更加稳健的风控指导。
  5. 策略账户可以记录自策略实例运行以来,所有的委托单和成交单历史记录,为策略的可视化提供后台支持
  6. 策略账户面对的策略不只是CTA策略,其它应用也可以使用。
  7. 策略账户可以计算策略的每笔历史交易盈亏,当前交易的保证金和手续费,策略账户可以让你实时低知道这些结果。

2. 策略账户长的啥样子?

本人已经提供长时间的构思和设计,经过一段时间的调试和运行,目前策略账户已经可以正常运行。现在展示如下:

description

3. 一点希望:希望vnpy官方能够合并这个功能到vn.py中,以便让更多粉丝都能够享受到策略账户的好处。

© 2015-2022 微信 18391752892
备案服务号:沪ICP备18006526号

沪公网安备 31011502017034号

【用户协议】
【隐私政策】
【免责条款】