基于VNPY实现网格策略实盘(币圈)


目录

  • 基于VNPY实现网格策略实盘(币圈)
  • vnpy事件驱动框架
  • 交易所gateway
  • vnpy算法引擎
  • vnpy数据格式
  • algo类和算法模板template
  • 网格交易策略逻辑
  • 程序入口
  • 策略实战



在回测程序中摸爬滚打了几个月,现在发现vnpy作为实盘系统,非常方便。

vnpy事件驱动框架

首先我们要利用到vnpy的事件驱动框架,是一个消息队列。其中,交易所gateway就是事件的生产者,算法模块AlgotradingApp是队列的消费者。为了能够获取行情,我们要让网格策略算法监听委托、成交回报、订单回报、行情tick这几种事件。

  1. tick、trade、order事件
    在交易所gateway中,on_tick方法可以将tick数据推送到事件引擎中,其他事件以此类推:
def on_tick(self, tick: TickData) -> None:
        """
        Tick event push.
        Tick event of a specific vt_symbol is also pushed.
        """
        self.on_event(EVENT_TICK, tick)
        self.on_event(EVENT_TICK + tick.vt_symbol, tick)

在事件的消费者算法引擎中,需要注册此事件即可获得tick的数据,并给此事件准备好handler函数:

def register_event(self):
        """"""
        self.event_engine.register(EVENT_TICK, self.process_tick_event)
        self.event_engine.register(EVENT_TIMER, self.process_timer_event)
        self.event_engine.register(EVENT_ORDER, self.process_order_event)
        self.event_engine.register(EVENT_TRADE, self.process_trade_event)

tick事件的handler函数如下:

def process_tick_event(self, event: Event):
        """"""
        tick = event.data

        algos = self.symbol_algo_map.get(tick.vt_symbol, None)
        if algos:
            for algo in algos:
                algo.update_tick(tick)

首先它会取出data,然后将此数据映射到相应的算法中。

交易所gateway

交易所的gateway就是程序与交易数据收发的入口,里面集成了rest、websocket、行情、交易的接口。本文使用的okex交易所的gateway。交易所的各种数据就是通过各种on开头的回调函数传递到事件引擎中,再被算法引擎监听获得。

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

        self.rest_api: "OkexRestApi" = OkexRestApi(self)
        self.ws_public_api: "OkexWebsocketPublicApi" = OkexWebsocketPublicApi(self)
        self.ws_private_api: "OkexWebsocketPrivateApi" = OkexWebsocketPrivateApi(self)

vnpy算法引擎

算法引擎负责处理各种事件的数据并映射到对应的算法中,首先来看看其初始化函数:

class AlgoEngine(BaseEngine):
    """"""
    setting_filename = "algo_trading_setting.json"

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

        self.algos = {}
        
        self.symbol_algo_map = {}
        self.orderid_algo_map = {}

        self.algo_templates = {}
        self.algo_settings = {}
        self.load_algo_template()
        self.register_event()

其中有两个比较重要的字典,用来映射订单、行情到对应的algo类中,每个algo类就是不同的算法。

vnpy数据格式

vnpy的所有事件数据都有它自己的dataclass,比如交易数据TradeData:

@dataclass
class TradeData(BaseData):
    """
    Trade data contains information of a fill of an order. One order
    can have several trade fills.
    """

    symbol: str
    exchange: Exchange
    orderid: str
    tradeid: str
    direction: Direction = None

    offset: Offset = Offset.NONE
    price: float = 0
    volume: float = 0
    datetime: datetime = None

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

可以看到我们可以通过trade数据得知这个交易明细的交易所,订单号,价格,数量,时间,这些都是算法类需要利用到的数据

algo类和算法模板template

为了实现自己编写的算法,我们需要写一个算法类,所有编写的算法都需要继承算法模板AlgoTemplate类,从而与算法引擎进行交互。

class GridAlgo(AlgoTemplate):
    """"""
    display_name = "Grid 网格交易"
    default_setting = {
        "vt_symbol": "",
        "upper_limit": "",
        "lower_limit": "",
        "investment": "",
        "grid_step": 0.0,
        "grid_levels":0,
        "interval": 10,
        "stop_loss": 0,
        "trailing_up":False,
    }

算法类再继承AlgoTemplate之后,就可以开始编写策略逻辑。在算法引擎中收到的Trade、Order事件数据都可以在算法类中利用on_trade,on_order进行处理。
更新算法的仓位数据时,利用on_trade方法:

def on_trade(self, trade: TradeData):
        """"""
        if trade.direction == Direction.LONG:
            self.pos += trade.volume
        else:
            self.pos -= trade.volume

多单LONG仓位增加,空单SHORT仓位减小。至于策略逻辑可以写在on_timer函数中,定时根据行情检测买卖条件。

网格交易策略逻辑

创建好了algo类接下来就可以编写策略逻辑,为了实现网格交易首先我们需要下限价单,根据网格上界与下界,以及制定好的间隔下单:

def send_all_limit_orders(self, tick):

        # 先购买足够多的币用来下网格卖单
        grid_price = []
        for price in np.arange(self.lower_limit, self.upper_limit,
                               (self.upper_limit - self.lower_limit) / self.grid_levels):
            grid_price.append(price)

        # sell_order_count = 0
        sell_order_amount = 0
        for price in grid_price:
            if price > tick.last_price:
                sell_order_amount += self.grid_usdt_amounts

        # 为了能够刚好下完所有限价,多买入2%的币
        buy_amount = sell_order_amount*1

        self.buy(
            self.vt_symbol,
            price=tick.ask_price_1,
            volume=round(buy_amount, 5),
            order_type=OrderType.MARKET
        )

        time.sleep(3)
        print(f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'):}买入初始仓位完成")

        # 开始下限价单
        if len(self.active_orders) < self.grid_levels:
            for price in np.arange(self.lower_limit, self.upper_limit, (self.upper_limit-self.lower_limit)/self.grid_levels):
                if price < tick.last_price:
                    volume = self.grid_usdt_amounts
                    orderid = self.buy(
                        volume=round(volume/price, 5),
                        price=round(price,5),
                        vt_symbol=self.vt_symbol
                    )
                else:
                    volume = self.grid_usdt_amounts
                    orderid = self.sell(
                        volume=round(volume/price, 5),
                        price=round(price,5),
                        vt_symbol=self.vt_symbol
                    )
                self.order_book[orderid] = round(price,5)

                if tick.gateway_name == 'BINANCE':
                    # 币安10秒内最多50个委托
                    time.sleep(0.3)
                if tick.gateway_name == 'OKEX':
                    # 币安10秒内最多50个委托
                    time.sleep(0.1)
        return True

成功下单之后我们就需要利用on_timer函数定时检测是否有网格成交并且填充网格:

def on_timer(self):
        """"""
        tick = self.get_tick(self.vt_symbol)

        if not tick:
            print(f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}:暂未收到行情")
            return

        if not self.is_init:
            if self.is_sbot:
                self.send_all_limit_orders_sbot(tick)
            else:
                self.send_all_limit_orders(tick)

            self.is_init = True

        # 最多容忍2个订单差
        if not self.is_all_sent:
            if len(self.order_book) - len(self.active_orders) >= 2:
                print(f"本地订单簿长度{len(self.order_book)},在线订单{len(self.active_orders)}")
                print(f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}:所有限价单订单回报未完全更新")
                return
            else:
                print(f"本地订单簿长度{len(self.order_book)},在线订单{len(self.active_orders)}")
                self.is_all_sent = True

        if tick.last_price <= self.stop_loss:
            # 到达止损价,即使止损
            print(f"达到止损价{self.stop_loss}, 撤销所有限价单,卖出现有仓位")
            self.cancel_all()
            self.sell(
                volume=self.pos,
                price=tick.ask_price_5,
                vt_symbol=self.vt_symbol
            )

        if not self.is_sbot:
            if self.is_all_sent:
                self.check_grid_count(tick)
        else:
            if self.is_all_sent:
                self.check_grid_count_sbot(tick)

如果符合条件就填充网格

def check_grid_count_sbot(self, tick):
        print(f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}:现在活跃订单簿数量:{len(self.active_orders)},本地订单数量{len(self.order_book)}")

        # 每五分钟检测一次网格是否未填充
        if self.stuck:
            return
        self.stuck = True
        diff = self.order_book.keys() - self.active_orders.keys()
        # 找出离lasttick最近的网格的index,对其暂不填充
        price_diff = []
        for order in diff:
            price = self.order_book[order]
            price_diff.append(abs(price-tick.last_price))

        if len(price_diff) == 0:
            return

        index_min = price_diff.index(min(price_diff))

        index_count = 0
        if len(self.order_book.keys()) > len(self.active_orders):
            for order in diff:
                if index_count == index_min:
                    print(f"{self.order_book[order]}此价格网格离lastprice过近,不进行委托")
                    index_count += 1
                    continue
                index_count += 1
                price = self.order_book[order]
                del self.order_book[order]

                if tick.last_price > price:
                    # print(f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'):}触碰了价格为{price}委托单")
                    volume = self.grid_usdt_amounts
                    orderid = self.buy(
                        volume=round(volume/price, 5),
                        price=price,
                        vt_symbol=self.vt_symbol
                    )
                else:
                    # print(f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'):}触碰了价格为{price}委托单")
                    volume = self.grid_usdt_amounts
                    volume = min(self.pos, round(volume/price, 5))
                    orderid = self.sell(
                        volume=volume,
                        price=price,
                        vt_symbol=self.vt_symbol
                    )
                self.order_book[orderid] = price

程序入口

首先实例化事件引擎以及主引擎:

event_engine = EventEngine()
    main_engine = MainEngine(event_engine)

首先我们需要往主引擎中添加gateway。

main_engine.add_gateway(OkexGateway)
    main_engine.add_gateway(HuobiGateway)
    main_engine.add_gateway(BinanceGateway)

为了实时获取行情我们需要订阅symbol,这里我们选择XRP/USDT币对,oms引擎会储存一个gateway中所有的contract,于是我们遍历这个contract列表,找出xrp现货。

contracts = main_engine.engines["oms"].contracts

    spotsymbol = []
    for contract_name in contracts:
        contract = \
            contracts[contract_name]
        vt_symbol = contract.vt_symbol
        if contract.product == Product.SPOT:
            if (arbsymbol in vt_symbol and 'usdt' in vt_symbol) or (arbsymbol.upper() in vt_symbol and 'USDT' in vt_symbol) :
                spotsymbol.append(vt_symbol)

手动设置好算法的参数:

up = 0.85
    down = 0.71
    grid_levels = 180
    grid_step = (up-down)/down/grid_levels
    grid_amounts = 45
    grid_usdt_amount = 14
    investment = grid_levels*grid_usdt_amount
    is_sbot = True

    default_setting = {
        "vt_symbol": vt_symbol,
        "upper_limit": up,
        "lower_limit": down,
        "investment": investment,
        "grid_step": grid_step,
        "grid_levels":grid_levels,
        "grid_amounts":grid_amounts,
        "grid_usdt_amounts":grid_usdt_amount,
        "is_sbot":is_sbot,
        "interval": 0,
        "stop_loss": 0.20,
        "trailing_up":False,
    }

最后添加算法引擎并初始化运行。

algoengine = main_engine.add_app(AlgoTradingApp)
    algoengine.init_engine()
    algoengine.start_algo(default_setting)

策略实战

参数选择:
选择了btc现货,底部31000顶部40000,180个网格的参数,每个限价单价值11usdt。

vt_symbol = 'BTC-USDT.OKEX'
    up = 40000
    down = 31000
    grid_levels = 180
    grid_step = (up-down)/down/grid_levels
    grid_amounts = 45
    grid_usdt_amount = 11
    investment = grid_levels*grid_usdt_amount

下所有限价单:

python网格交易的策略 vnpy 网格交易_python网格交易的策略

策略输出

2021-06-21 15:09:26:现在活跃订单簿数量:178,本地订单数量180
2021-06-21 15:09:27:现在活跃订单簿数量:178,本地订单数量180
多单成交,数量:0.00033,价格:33000.0
有不同的网格成交了,将stuck取消
2021-06-21 15:09:28:现在活跃订单簿数量:177,本地订单数量180
GridAlgo_1:委托卖出BTC-USDT.OKEX:0.00033@33050.0
33000.0此价格网格离lastprice过近,不进行委托
GridAlgo_1:委托卖出BTC-USDT.OKEX:0.00033@33100.0
2021-06-21 15:09:29:现在活跃订单簿数量:179,本地订单数量180
2021-06-21 15:09:30:现在活跃订单簿数量:179,本地订单数量180
......

将所有成交连点成线:

python网格交易的策略 vnpy 网格交易_算法类_02

可以看出策略实现了逢低买入逢高卖出。