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

arnego wrote:

xiaohe wrote:

  1. 是有rqdata账户吗?
  2. 上海证券交易所在rqdata好像是XSHG

谢谢回复.
有rqdata的,期货一直使用没有问题.
SSE/SZSE是XTP的后缀,登录XTP的情况下,用xshg会这样

description

你的米筐账户如果费用是3000元一年,通常只有国内期货的数据服务权限,是没有国内A股数据服务的。

1 vnpy中CTA回测是一个没有保证金参数的回测

1.1 看看BacktesterEngine的run_backtesting()函数

这其中没有保证金参数,这会导致回测可以开得了的仓位,实盘却不一定可以!

   def run_backtesting(
        self,
        class_name: str,
        vt_symbol: str,
        interval: str,
        start: datetime,
        end: datetime,
        rate: float,
        slippage: float,
        size: int,
        pricetick: float,
        capital: int,
        inverse: bool,
        setting: dict
    ):
        """"""
        self.result_df = None
        self.result_statistics = None

        engine = self.backtesting_engine
        engine.clear_data()

        engine.set_parameters(
            vt_symbol=vt_symbol,
            interval=interval,
            start=start,
            end=end,
            rate=rate,
            slippage=slippage,
            size=size,
            pricetick=pricetick,
            capital=capital,
            inverse=inverse
        )

        strategy_class = self.classes[class_name]
        engine.add_strategy(
            strategy_class,
            setting
        )

        engine.load_data()

        try:
            engine.run_backtesting()
        except Exception:
            msg = f"策略回测失败,触发异常:\n{traceback.format_exc()}"
            self.write_log(msg)

            self.thread = None
            return

        self.result_df = engine.calculate_result()
        self.result_statistics = engine.calculate_statistics(output=False)

        # Clear thread object handler.
        self.thread = None

        # Put backtesting done event
        event = Event(EVENT_BACKTESTER_BACKTESTING_FINISHED)
        self.event_engine.put(event)

2 vnpy中CTA回测认为你想买就一定能够买到

2.1 先看看BacktestingEngine的限价委托单的撮合函数cross_limit_order()

    def cross_limit_order(self):
        """
        Cross limit order with last bar/tick data.
        """
        if self.mode == BacktestingMode.BAR:
            long_cross_price = self.bar.low_price
            short_cross_price = self.bar.high_price
            long_best_price = self.bar.open_price
            short_best_price = self.bar.open_price
        else:
            long_cross_price = self.tick.ask_price_1
            short_cross_price = self.tick.bid_price_1
            long_best_price = long_cross_price
            short_best_price = short_cross_price

        for order in list(self.active_limit_orders.values()):
            # Push order update with status "not traded" (pending).
            if order.status == Status.SUBMITTING:
                order.status = Status.NOTTRADED
                self.strategy.on_order(order)

            # Check whether limit orders can be filled.
            long_cross = (
                order.direction == Direction.LONG
                and order.price >= long_cross_price
                and long_cross_price > 0
            )

            short_cross = (
                order.direction == Direction.SHORT
                and order.price <= short_cross_price
                and short_cross_price > 0
            )

            if not long_cross and not short_cross:
                continue

            # Push order udpate with status "all traded" (filled).
            order.traded = order.volume
            order.status = Status.ALLTRADED
            self.strategy.on_order(order)

            self.active_limit_orders.pop(order.vt_orderid)

            # Push trade update
            self.trade_count += 1

            if long_cross:
                trade_price = min(order.price, long_best_price)
                pos_change = order.volume
            else:
                trade_price = max(order.price, short_best_price)
                pos_change = -order.volume

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

            self.strategy.pos += pos_change
            self.strategy.on_trade(trade)

            self.trades[trade.vt_tradeid] = trade

2.2 再看看BacktestingEngine的停止单撮合函数cross_stop_order()

    def cross_stop_order(self):
        """
        Cross stop order with last bar/tick data.
        """
        if self.mode == BacktestingMode.BAR:
            long_cross_price = self.bar.high_price
            short_cross_price = self.bar.low_price
            long_best_price = self.bar.open_price
            short_best_price = self.bar.open_price
        else:
            long_cross_price = self.tick.last_price
            short_cross_price = self.tick.last_price
            long_best_price = long_cross_price
            short_best_price = short_cross_price

        for stop_order in list(self.active_stop_orders.values()):
            # Check whether stop order can be triggered.
            long_cross = (
                stop_order.direction == Direction.LONG
                and stop_order.price <= long_cross_price
            )

            short_cross = (
                stop_order.direction == Direction.SHORT
                and stop_order.price >= short_cross_price
            )

            if not long_cross and not short_cross:
                continue

            # Create order data.
            self.limit_order_count += 1

            order = OrderData(
                symbol=self.symbol,
                exchange=self.exchange,
                orderid=str(self.limit_order_count),
                direction=stop_order.direction,
                offset=stop_order.offset,
                price=stop_order.price,
                volume=stop_order.volume,
                status=Status.ALLTRADED,
                gateway_name=self.gateway_name,
                datetime=self.datetime
            )

            self.limit_orders[order.vt_orderid] = order

            # Create trade data.
            if long_cross:
                trade_price = max(stop_order.price, long_best_price)
                pos_change = order.volume
            else:
                trade_price = min(stop_order.price, short_best_price)
                pos_change = -order.volume

            self.trade_count += 1

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

            self.trades[trade.vt_tradeid] = trade

            # Update stop order.
            stop_order.vt_orderids.append(order.vt_orderid)
            stop_order.status = StopOrderStatus.TRIGGERED

            if stop_order.stop_orderid in self.active_stop_orders:
                self.active_stop_orders.pop(stop_order.stop_orderid)

            # Push update to strategy.
            self.strategy.on_stop_order(stop_order)
            self.strategy.on_order(order)

            self.strategy.pos += pos_change
            self.strategy.on_trade(trade)

结论:它们的撮合机制都是只考虑了价格,没有考虑盘口是否允许

3 vnpy中CTA回测假定认为你永远都有100万的资金

如果你的策略是动态开仓的,你策略中能够引用的参考资金就是回测开始界面中的100万(如果你修改了,或者是其他的值)。因为回测引擎没有把自回测开始的交易中盈亏统计结果回馈给用户策略,因为现在也没有提供这样的接口。
这在实盘中是不可能不考虑的,你投入交易的资金一定是有限的,随着策略的运行,资金权益是会变动的。如果赚钱了,当然不会影响策略运行,只是资金的利用效率不够高而已;如果亏钱了,就可能会影响策略的下单,都快亏光了的资金,还能够按照你原来的资金下单吗?答案是:不可能的!

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

用Python的交易员 wrote:

接下来就可以本地生成主力合约了嘛

是呀,也可以实现自动换约。

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

量甫 wrote:

hxxjava wrote:

量甫 wrote:

请教大咖:下面的有关数据不能从vnpy的全局配置文件和策略的配置文件中读取得到吗?

if name == "main":
rq.init('xxxxx','**',("rqdatad-pro.ricequant.com",16011))

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

这里是单独对TradeHours进行测试,不启动vnpy也可以测试。
谢谢!那在是盘中怎么应用呀?请示教。

class RBreakStrategy2(CtaTemplate):
    ... ...
    def __init__(self, cta_engine, strategy_name, vt_symbol, setting):
        """"""
        super(RBreakStrategy2, self).__init__(
            cta_engine, strategy_name, vt_symbol, setting
        )

        self.bg = BarGenerator(self.on_bar)
        self.am = ArrayManager()
        self.bars = []

        symbol,exchange = vt_symbol.split('.')
        self.trade_hour = TradeHours(symbol)   # 这里就是应用方法
        self.trade_datetimes = None
        self.exit_time = None

量甫 wrote:

请教大咖:下面的有关数据不能从vnpy的全局配置文件和策略的配置文件中读取得到吗?

if name == "main":
rq.init('xxxxx','**',("rqdatad-pro.ricequant.com",16011))

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

这里是单独对TradeHours进行测试,不启动vnpy也可以测试。

ljx wrote:

好多不懂的地方想和你交流,我的QQ125894615
点击用户可以发私信,描述下问题

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测试网关,填写之前的测评设置参数,就可以顺利登录期货公司的测评环境了。

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

沪公网安备 31011502017034号

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