Implementation of grid strategy real offer based on VNPY

Posted by Jakei on Fri, 28 Jan 2022 05:59:56 +0100

Implementation of grid strategy real offer based on VNPY


After several months of trial and error in the back test program, I find vnpy as a solid offer system is very convenient.

vnpy event driven framework

First, we need to use vnpy's event driven framework, which is a message queue. Among them, the exchange gateway is the producer of events, and the AlgotradingApp algorithm module is the consumer of queues. In order to obtain the market, we need to let the grid strategy algorithm monitor the events of entrustment, transaction return, order return and market tick.

  1. tick, trade and order events
    In the exchange gateway, on_ The tick method can push the tick data to the event engine, and so on for other events:
    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)

In the consumer algorithm engine of the event, you need to register this event to obtain the data of the tick, and prepare the handler function for this event:

    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)

The handler function of the tick event is as follows:

    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)

First, it will take out the data, and then map the data to the corresponding algorithm.

Exchange gateway

The gateway of the exchange is the entrance for sending and receiving programs and trading data, which integrates the interfaces of rest, websocket, quotation and trading. This paper uses the gateway of okex exchange. All kinds of data of the exchange are passed to the event engine through various callback functions starting with on, and then monitored by the algorithm engine.

    def __init__(self, event_engine: EventEngine, gateway_name: str = "OKEX") -> None:
        """Constructor"""
        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 algorithm engine

The algorithm engine is responsible for processing the data of various events and mapping them to the corresponding algorithm. First, let's take a look at its initialization function:

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

There are two important dictionaries used to map orders and quotations to the corresponding algo classes. Each algo class is a different algorithm.

vnpy data format

All event data of vnpy has its own dataclass, such as transaction data 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}"

It can be seen that we can know the transaction details through trade data, including the exchange, order number, price, quantity and time. These are the data that the algorithm class needs to use

algo class and algorithm template

In order to implement the algorithm written by ourselves, we need to write an algorithm class. All the algorithms written need to inherit the AlgoTemplate class of the algorithm template, so as to interact with the algorithm engine.

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

After the algorithm class inherits AlgoTemplate, you can start writing policy logic. The Trade and order event data received in the algorithm engine can be used in the algorithm class_ Trade,on_order.
When updating the position data of the algorithm, use on_trade method:

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

The LONG position of multiple orders increases and the SHORT position of empty orders decreases. The strategy logic can be written on_ In the timer function, the trading conditions are detected regularly according to the market.

Grid transaction strategy logic

After the algo class is created, we can write the policy logic. In order to realize the grid transaction, we first need to place the lower limit order according to the upper and lower bounds of the grid and the established interval:

    def send_all_limit_orders(self, tick):

        # First buy enough coins for grid sales orders
        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

        # In order to just finish all the price limits, buy 2% more coins
        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'):}Completion of initial buying position")

        # Start limit order
        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':
                    # Up to 50 commissions in 10 seconds
                    time.sleep(0.3)
                if tick.gateway_name == 'OKEX':
                    # Up to 50 commissions in 10 seconds
                    time.sleep(0.1)
        return True

After successfully placing an order, we need to use on_ The timer function regularly detects whether there is a grid transaction and fills the grid:

    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')}:No quotation has been received yet")
            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

        # Tolerance of up to 2 orders
        if not self.is_all_sent:
            if len(self.order_book) - len(self.active_orders) >= 2:
                print(f"Local order book length{len(self.order_book)},Online order{len(self.active_orders)}")
                print(f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}:All limit order returns have not been fully updated")
                return
            else:
                print(f"Local order book length{len(self.order_book)},Online order{len(self.active_orders)}")
                self.is_all_sent = True

        if tick.last_price <= self.stop_loss:
            # Reach the stop loss price, even if the stop loss
            print(f"Stop loss price reached{self.stop_loss}, Cancel all price limit orders and sell existing positions")
            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)

Fill the mesh if the conditions are met

    def check_grid_count_sbot(self, tick):
        print(f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}:Current active order book quantity:{len(self.active_orders)},Local order quantity{len(self.order_book)}")

        # Check whether the grid is unfilled every five minutes
        if self.stuck:
            return
        self.stuck = True
        diff = self.order_book.keys() - self.active_orders.keys()
        # Find the index of the grid closest to lasttick and do not fill it temporarily
        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]}This price is off grid lastprice Too close, no commission")
                    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'):} touched a commission order with a price of {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'):} touched a commission order with a price of {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

Program entry

First instantiate the event engine and the main engine:

    event_engine = EventEngine()
    main_engine = MainEngine(event_engine)

First, we need to add a gateway to the main engine.

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

In order to get the market in real time, we need to subscribe to the symbol. Here we choose XRP/USDT currency pair. The oms engine will store all contracts in a gateway, so we traverse the contract list to find out the xrp spot.

   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)

Manually set the parameters of the algorithm:

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

Finally, add the algorithm engine and initialize the operation.

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

Strategy practice

Parameter selection:
The parameters of btc spot, 31000 grids at the bottom and 40000180 grids at the top are selected, and the value of each limit order is 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

All price limit orders:

Policy output

2021-06-21 15:09:26:Current active order book quantity: 178, local order quantity: 180
2021-06-21 15:09:27:Current active order book quantity: 178, local order quantity: 180
 Multi order transaction, quantity: 0.00033,Price: 33000.0
 There are different grid transactions, which will stuck cancel
2021-06-21 15:09:28:Current active order book quantity: 177, local order quantity: 180
GridAlgo_1: Entrusted sale BTC-USDT.OKEX: 0.00033@33050.0
33000.0 This price is off grid lastprice Too close, no commission
GridAlgo_1: Entrusted sale BTC-USDT.OKEX: 0.00033@33100.0
2021-06-21 15:09:29:Current active order book quantity: 179, local order quantity: 180
2021-06-21 15:09:30:Current active order book quantity: 179, local order quantity: 180
......

Connect all transactions into a line:

It can be seen that the strategy realizes buying on the low and selling on the high.

Topics: Python