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