“Penny Jump” Commodity Futures High Frequency Trading Strategy written by C++
The market is the battleground, the buyer and the seller are always in the game, which is also the eternal theme of the trading business. The Penny Jump strategy shared today is one of high-frequency strategies, originally derived from the interbank foreign exchange market, and is often used in mainstream fait currency pairs.
In high-frequency trading, there are two main types of strategies. The buyer side strategy and the seller side strategy. The seller side strategy is usually a market-making strategy, and the these two side of strategies are opponents. For example, the high-frequency arbitrage buyer strategy of smoothing all unreasonable phenomena in the market at the fastest speed, taking the initiative to attack price quickly, or eating the wrong price of other market makers.
There is also a way to analyze the historical data or the ordering rules of the market, to send the pending orders at an unreasonable price in advance, and to send the withdrawal orders with the rapid change of the market price. Such strategies are common in passive market making, once the pending orders are executed, And after a certain profit or after reaching the stop-loss condition, the position will be closed. Passive market-making strategies usually do not require too much speed, but it requires a strong strategy logic and structure.
Penny Jump translates into English is the meaning of micro-price increase. The principle is to track the buying price and the selling price of the market. Then, according to the price of the market, plus or minus the micro-price increase of the tracking price, it is obvious that this is a passive trading strategy, it is belong to the seller side market-making strategy. Its business model and logic are to conduct bilateral transactions on exchange-listed limit orders to provide liquidity.
The market-making strategy requires a certain amount of inventory in hand, and then trades both at the buyer and seller side. The main income of this strategy is the commission fee return provided by the exchange, as well as the price difference earned by buying low and selling high. But for many high-frequency traders who want to market making, earning a bid-ask spread is a good thing, but it is not an absolute means of profit.
We know that there are many retail investors in the trading market, and there are also many large investors, such as: “hot money”, public funds, private funds, and so on. Retail investors usually have less funds, their orders has very small impact in the market, they can easily buy and sell a trading target at any time. But for large funds to participate the market, it is not that simple.
If a large investor wants to buy 500 lots of crude oil, there are not so many orders at the current price for sell, and the investor don’t want to buy them at the higher price. If they insist on send the buying order at current price, The cost of slippage points will be too much. therefore, it has to send a pending order at the desired price. All the participants in the market will see a hugh buying order showing on the certain price.
Because of this huge order, it looks clumsy in the market, sometimes we call it “elephant orders”. For example, the current market showing:
Selling Price 400.3, Order volume 50; buying price 400.1, Order volume 10.
Suddenly this cumbersome elephant jump into the market, and the bid price was sent at the price of 400.1. At this time, the market becomes:
Selling Price 400.3, Order volume 50; Buying price 400.1, Order volume 510.
Traders all know that if there is a huge amount of pending orders at certain price, then this price will form a strong support(or resistance). Futhermore, high frequency traders also know, if they send a buying order above the “Buying 1” price in the order book depth, then the market becomes:
Selling Price 400.3, Order volume 50; Buying price 400.2, Order volume 1,
the price 400.1 becomes “Buying 2” price in the order book depth. Then if the price rises to 400.3, the high-frequency trader will earn a profit of 0.1.
Even if the price does not rise, in the position of “buying 2”, there still has an “elephant” holding the price, and it can be quickly sold back to the elephant at price 400.1. This is the general idea of Penny Jump strategy. Its logic is as simple as this, by monitoring the market order status, to speculate the opponent’s intentions, and then to take the lead in building a favorable position, and finally profit from a small spread in a short period of time. For this “elephant”, because he hangs a huge amount of buying order in the market, he exposed his trading intentions, and naturally became the hunted target of high-frequency traders.
First, observe the trading opportunities with very low probability of the market, and make corresponding strategies according to the trading logic. If the logic is complex, you need to use the existing mathematical knowledge, use the model to describe the nature of the irrational phenomenon as much as possible, and minimize the overfitting. In addition, it must be verified by a backtest engine that can meet the “Price first then Volume first” principle. Luckily, we have FMZ Quant platform that currently supports this backtesting modes.
What is the meaning of supporting “Price first then Volume first” backtest engine? You can understand it as: you send a pending order at 400.1 to buy, only when the selling price in order book depth is 400.1 or lower, your pending order can be closed(executed). It only calculates the price data of the pending orders, and does not calculate the volume data of pending orders, which only meets the price priority(price first) in the exchange orders matching rules.
The “Volume first” is an upgraded version of the “price first”. It is both price-prioritized and time-first. It can be said that this matching mode is exactly the same as the exchange model. By calculating the amount of the pending order, it is judged whether the current pending order reaches the condition of passive transaction to realize the volume transaction, so as to achieve a real simulation of the real market environment.
In addition, some readers may find that the Penny Jump strategy requires market trading opportunities, that is, the market need has at least two “hop” price gaps. Under normal circumstances, the main trading contract of commodity futures is relatively “busy”. The difference between “Buying 1” and “Selling 1” hop is that there is almost no trading chance. So we put our energy on the sub-primary contract where the trading is not too active. This type of trading contract occasionally has two or even three “hop” opportunities. For example, in the MA (“Methanol” code in Chinese commodity futures) 1909 contract, the following situation occurs:
“Selling 1” price 2225 with volume 551, “Buying 1” price 2223 with volume 565, look down for a few seconds. After this happens, it will disappear after several ticks. In this case, we regard the market as self-correction. What we have to do is to catch up. Before the market actively corrects it. if we do it by manually, it would be impossible, with the help of automatic trading, we can make it possible.
The two “hop” price gap appearance situation happens very often, but the three hops are the safest, but the three hops rarely occur, resulting in trading frequency is too low.
Next, we observe the difference between the pervious “Selling 1” “Buying 1” and the “Buying 1” “Selling 1” now. In order to fill the price gap between the market, if the speed is fast enough, it can be placed at the forefront of other orders. Also, the position holding time is very short, with this trading logic, after the realization of the strategy, take the MA909 as an example, the real market test recommends Esunny instead of the CTP interface, the mechanism of position and fund changing situation for Esunny is by pushed data, very suitable for high frequency trading.
After clearing the trading logic, we can use the code to achieve it. Since the FMZ Quant platform use C++ writing strategy examples are too few, here we use C++ to write this strategy, which is convenient for everyone to learn, and the variety is commodity futures. First open: fmz.com > Login > Dashboard > strategy library > New Strategy > Click the drop-down menu in the top left corner > Select C++ to start writing the strategy, pay attention to the comments in the code below.
Next, let’s look at the HFT class, which has five methods. The first method is the construction method; the second method is to obtain the current day of the week to determine whether it is a new K line; the third method is mainly to cancel all unfilled orders, and get detailed Position information, because before the order is placed, it must first determine the current position status; the fourth method is mainly used to print some information, for this strategy, this method is not the main one; the most important is the fifth method, This method is mainly responsible for processing trading logic and placing orders.
/ / Define the HFT class
Class HFT {
Public:
HFT() {
// Constructor
}
Int getTradingWeekDay() {
// Get the current day of the week to determine if it is a new K line
}
State getState() {
/ / Get order data
}
Void stop() {
// Print orders and positions
}
Bool Loop() {
// Strategy logic and placing orders
}
};
// main function
Void main() {
LogReset(); // clear the log
SetErrorFilter("ready|timeout"); // Filter error messages
Log("Init OK"); // Print the log
HFT hft; // Create HFT object
While (hft.Loop()); // enter loop
Log("Exit"); // Program exits, prints the log
}
So let’s see how each of the methods in this HFT class is implemented, and how the most core Loop method works. From top to bottom, we will implement the specific implementation of each method one by one, and you will find that the original high frequency strategy is so simple. Before talking about the HFT class, we first defined several global variables for storing the results of the hft object calculation. They are: storing order status, position status, holding long position, holding short position, buying price, buying quantity, selling price, selling quantity. Please see the code below:
/ / Define the global enumeration type State
Enum State {
STATE_NA, // store order status
STATE_IDLE, // store position status
STATE_HOLD_LONG, // store long position directions
STATE_HOLD_SHORT, // store short position direction
};
/ / Define global floating point type variable
Typedef struct {
Double bidPrice; // store the buying price
Double bidAmount; // store the buying amount
Double askPrice; // store the selling price
Double askAmount; // store the selling amount
} Book;
With the above global variables, we can store the results calculated by the hft object separately, which is convenient for subsequent calls by the program. Next we will talk about the specific implementation of each method in the HFT class. First, the first HFT method is a constructor that calls the second getTradingWeekDay method and prints the result to the log. The second getTradingWeekDay method gets the current day of the week to determine if it is a new K line. It is also very simple to implement, get the timestamp, calculate the hour and week, and finally return the number of weeks; the third getState method is a bit long, i will just describe the general idea, for specific explanation, you can look at the comments in the following coding block.
Next, let’s get all the orders first, return the result is a normal array, then traverse this array, one by one to cancel the order, then get the position data, return an array, and then traverse this Array, get detailed position information, including: direction, position, yesterday or current position, etc., and finally return the result; the fourth stop method is to print information; the code is as follows:
Public:
// Constructor
HFT() {
_tradingDay = getTradingWeekDay();
Log("current trading weekday", _tradingDay);
}
// Get the current day of the week to determine if it is a new K line
Int getTradingWeekDay() {
Int seconds = Unix() + 28800; // get the timestamp
Int hour = (seconds/3600)%24; // hour
Int weekDay = (seconds/(60*60*24))%7+4; // week
If (hour > 20) {
weekDay += 1;
}
Return weekDay;
}
/ / Get order data
State getState() {
Auto orders = exchange.GetOrders(); // Get all orders
If (!orders.Valid || orders.size() == 2) { // If there is no order or the length of the order data is equal to 2
Return STATE_NA;
}
Bool foundCover = false; // Temporary variable used to control the cancellation of all unexecuted orders
// Traverse the order array and cancel all unexecuted orders
For (auto &order : orders) {
If (order.Id == _coverId) {
If ((order.Type == ORDER_TYPE_BUY && order.Price < _book.bidPrice - _toleratePrice) ||
(order.Type == ORDER_TYPE_SELL && order.Price > _book.askPrice + _toleratePrice)) {
exchange.CancelOrder(order.Id, "Cancel Cover Order"); // Cancel order based on order ID
_countCancel++;
_countRetry++;
} else {
foundCover = true;
}
} else {
exchange.CancelOrder(order.Id); // Cancel order based on order ID
_countCancel++;
}
}
If (foundCover) {
Return STATE_NA;
}
// Get position data
Auto positions = exchange.GetPosition(); // Get position data
If (!positions.Valid) { // if the position data is empty
Return STATE_NA;
}
// Traverse the position array to get specific position information
For (auto &pos : positions) {
If (pos.ContractType == Symbol) {
_holdPrice = pos.Price;
_holdAmount = pos.Amount;
_holdType = pos.Type;
Return pos.Type == PD_LONG || pos.Type == PD_LONG_YD ? STATE_HOLD_LONG : STATE_HOLD_SHORT;
}
}
Return STATE_IDLE;
}
// Print orders and positions information
Void stop() {
Log(exchange.GetOrders()); // print order
Log(exchange.GetPosition()); // Print position
Log("Stop");
}
Finally, we focus on how the Loop function controls the strategy logic and the order. If you want to see more carefully, you can refer to the comments in the code. First determine whether the CTP transaction and the market server are connected; then obtain the available balance of the account and obtain the number of weeks; then set the variety code to be traded, by calling the FMZ Quant official SetContractType function, and can use this function to return the details of the trading variety; then call the GetDepth function to get the depth data of the current market. The depth data includes: buying price, buying volume, selling price, selling volume, etc., and we store them with variables, because they will be used later; Then output these port data to the status bar to facilitate the user to view the current market status; the code is as follows:
// Strategy logic and placing order
Bool Loop() {
If (exchange.IO("status") == 0) { // If the CTP and the quote server are connected
LogStatus(_D(), "Server not connect ...."); // Print information to the status bar
Sleep(1000); // Sleep 1 second
Return true;
}
If (_initBalance == 0) {
_initBalance = _C(exchange.GetAccount).Balance; // Get account balance
}
Auto day = getTradingWeekDay(); // Get the number of weeks
If (day != _tradingDay) {
_tradingDay = day;
_countCancel = 0;
}
// Set the futures contract type and get the contract specific information
If (_ct.is_null()) {
Log(_D(), "subscribe", Symbol); // Print the log
_ct = exchange.SetContractType(Symbol); // Set futures contract type
If (!_ct.is_null()) {
Auto obj = _ct["Commodity"]["CommodityTickSize"];
Int volumeMultiple = 1;
If (obj.is_null()) { // CTP
Obj = _ct["PriceTick"];
volumeMultiple = _ct["VolumeMultiple"];
_exchangeId = _ct["ExchangeID"];
} else { // Esunny
volumeMultiple = _ct["Commodity"]["ContractSize"];
_exchangeId = _ct["Commodity"]["ExchangeNo"];
}
If (obj.is_null() || obj <= 0) {
Panic("PriceTick not found");
}
If (_priceTick < 1) {
exchange.SetPrecision(1, 0); // Set the decimal precision of the price and the quantity of the order.
}
_priceTick = double(obj);
_toleratePrice = _priceTick * TolerateTick;
_ins = _ct["InstrumentID"];
Log(_ins, _exchangeId, "PriceTick:", _priceTick, "VolumeMultiple:", volumeMultiple); // print the log
}
Sleep(1000); // Sleep 1 second
Return true;
}
// Check orders and positions to set status
Auto depth = exchange.GetDepth(); // Get depth data
If (!depth.Valid) { // if no depth data is obtained
LogStatus(_D(), "Market not ready"); // Print status information
Sleep(1000); // Sleep 1 second
Return true;
}
_countTick++;
_preBook = _book;
_book.bidPrice = depth.Bids[0].Price; // "Buying 1" price
_book.bidAmount = depth.Bids[0].Amount; // "Buying 1" amount
_book.askPrice = depth.Asks[0].Price; // "Selling 1" price
_book.askAmount = depth.Asks[0].Amount; // "Selling 1" amount
// Determine the state of the port data assignment
If (_preBook.bidAmount == 0) {
Return true;
}
Auto st = getState(); // get the order data
// Print the port data to the status bar
LogStatus(_D(), _ins, "State:", st,
"Ask:", depth.Asks[0].Price, depth.Asks[0].Amount,
"Bid:", depth.Bids[0].Price, depth.Bids[0].Amount,
"Cancel:", _countCancel,
"Tick:", _countTick);
}
after done so much, we can finally placing orders. Before the trading, first we judge the current holding position status of the program (no holding position, long position orders, short position orders), here we used the if…else if…else if logic control. They are very simple, If there is no holding position, the position will be opened according the logic condition. If there is holding position, the position will closed according the logic condition. In order to facilitate everyone’s understanding, we use three paragraphs to explain the logic, For opening position part:
First declare a Boolean variable, we use it to control the closing position; next we need get the current account information, and record the profit value, then determine the status of the withdrawal order, if the number of withdrawal exceeds the set maximum, print the related information in the log; then calculate the absolute value of the current bid and offer price difference to determine whether there is more than 2 hops between the current bid price and the ask price.
Next, we get the “Buying 1” price and “Selling 1” price, if the previous buying price is greater than the current buying price, and the current selling volume is less than the buying volume, it means that “Buying 1” price is disappeared. the long position opening price and the order quantity are set; otherwise, if the previous selling price is less than the current selling price, and the current buying volume is less than The selling volume proves that the “Selling 1” price is disappeared, the short position opening price and the order quantity are set; in the end, the long position and the short position orders enter the market at the same time. The specific code is as follows:
Bool forceCover = _countRetry >= _retryMax; // Boolean value used to control the number of closings
If (st == STATE_IDLE) { // if there is no holding position
If (_holdAmount > 0) {
If (_countRetry > 0) {
_countLoss++; // failure count
} else {
_countWin++; // success count
}
Auto account = exchange.GetAccount(); // Get account information
If (account.Valid) { // If get account information
LogProfit(_N(account.Balance+account.FrozenBalance-_initBalance, 2), "Win:", _countWin, "Loss:", _countLoss); // Record profit value
}
}
_countRetry = 0;
_holdAmount = 0;
// Judging the status of withdrawal
If (_countCancel > _cancelMax) {
Log("Cancel Exceed", _countCancel); // Print the log
Return false;
}
Bool canDo = false; // temporary variable
If (abs(_book.bidPrice - _book.askPrice) > _priceTick * 1) { // If there is more than 2 hops between the current bid and ask price
canDo = true;
}
If (!canDo) {
Return true;
}
Auto bidPrice = depth.Bids[0].Price; // Buying 1 price
Auto askPrice = depth.Asks[0].Price; // Selling 1 price
Auto bidAmount = 1.0;
Auto askAmount = 1.0;
If (_preBook.bidPrice > _book.bidPrice && _book.askAmount < _book.bidAmount) { // If the previous buying price is greater than the current buying price and the current selling volume is less than the buying volume
bidPrice += _priceTick; // Set the opening long position price
bidAmount = 2; // set the opening long position volume
} else if (_preBook.askPrice < _book.askPrice && _book.bidAmount < _book.askAmount) { // If the previous selling price is less than the current selling price and the current buying volume is less than the selling volume
askPrice -= _priceTick; // set the opening short position volume
askAmount = 2; // set the opening short position volume
} else {
Return true;
}
Log(_book.bidPrice, _book.bidAmount, _book.askPrice, _book.askAmount); // Print current market data
exchange.SetDirection("buy"); // Set the order type to buying long
exchange.Buy(bidPrice, bidAmount); // buying long and open position
exchange.SetDirection("sell"); // Set the order type to selling short
exchange.Sell(askPrice, askAmount); // short sell and open position
}
Next, we will talk about how to close long position, first set the order type according to the current position status, and then get the “Selling 1” price. If the current “Selling 1” price is greater than the buying long opening price, set the closing long position price. If the current “selling 1” price is less than the long position opening price, then reset closing quantity variable to true, then close all long position. Coding part as below:
Else if (st == STATE_HOLD_LONG) { // if holding long position
exchange.SetDirection((_holdType == PD_LONG && _exchangeId == "SHFE") ? "closebuy_today" : "closebuy"); // Set the order type, and close position
Auto sellPrice = depth.Asks[0].Price; // Get "Selling 1" price
If (sellPrice > _holdPrice) { // If the current "selling 1" price is greater than the long position opening price
Log(_holdPrice, "Hit #ff0000"); // Print long position opening price
sellPrice = _holdPrice + ProfitTick; // Set closing long position price
} else if (sellPrice < _holdPrice) { // If the current "selling 1" price is less than the long position opening price
forceCover = true;
}
If (forceCover) {
Log("StopLoss");
}
_coverId = exchange.Sell(forceCover ? depth.Bids[0].Price : sellPrice, _holdAmount); // close long position
If (!_coverId.Valid) {
Return false;
}
}
Finally, let’s see how to close short position. The principle is the opposite of the above-mentioned closing long position. First, according to the current position status, set the order type, and then get the “Buying 1” price, if the current “buying 1” price is less than the short position opening price, the price of the closing short position will be set. If the current “buying 1” price is greater than the opening short position price, reset closing quantity variable to true, then close all short position.
Else if (st == STATE_HOLD_SHORT) { // if holding short position
exchange.SetDirection((_holdType == PD_SHORT && _exchangeId == "SHFE") ? "closesell_today" : "closesell"); // Set the order type, and close position
Auto buyPrice = depth.Bids[0].Price; // Get "buying 1" price
If (buyPrice < _holdPrice) { // If the current "buying 1" price is less than the opening short position price
Log(_holdPrice, "Hit #ff0000"); // Print the log
buyPrice = _holdPrice - ProfitTick; // Set the close short position price
} else if (buyPrice > _holdPrice) { // If the current "buying 1" price is greater than the opening short position price
forceCover = true;
}
If (forceCover) {
Log("StopLoss");
}
_coverId = exchange.Buy(forceCover ? depth.Asks[0].Price : buyPrice, _holdAmount); // close short position
If (!_coverId.Valid) {
Return false;
}
}
The above is a complete analysis of this strategy. Click here (https://www.fmz.com/strategy/163427) to copy the complete strategy source code without configuring backtest environment on FMZ Quant.
In order to satisfy the curiosity of high-frequency trading and to see the results more clearly, this strategy backtest environment transaction fee is set to 0, which leads to a simple fast speed logic. if you want to cover the transaction fee to achieve profitability in the real market. More optimization is needed. Such as using the order stream to carry out short-term forecasting to improve the winning rate, plus the exchange fee refund, In order to achieve a sustainable profitable strategy, there are many books on high-frequency trading. I hope that everyone can think more and go to the real market instead of just staying on the principle.
FMZ Quant is a purely technology-driven team that provides a highly efficient available backtest mechanism for quantitative trading enthusiasts. Our backtest mechanism is simulating a real exchange, rather than a simple price match. We hope users can take advantage of the platform to better play their own abilities.