Summary

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.

High frequency strategy classification

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.

What is the Penny Jump strategy?

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.

Penny Jump strategy principle

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.

"Penny Jump" strategy implementation

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.

Strategy code

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.

  • Step 1: First build the framework of the strategy, in which a HFT class and a main function are defined. The first line in the main function is to clear the log. The purpose of this is to clear the previously running log information every time the strategy is restarted. The second line is to filter some error messages that are not necessary, such as network delay and some tips appear, so that the log only records important information and looks more neat; the third line is to print the "Init OK" message, meaning that the program has started to start. the fourth line is to create an object according to the HFT class, and the object name is hft; the fifth line program enters the while loop, and always executes the loop in the object hft Method, it can be seen that the Loop method is the core logic of this program. Line 6 is another print message. Under normal circumstances, the program will not execute to line 6. if the program is executed to line 6, the proof program has ended.

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.

Backtest results

Trading logic

Strategy statement

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.

About us

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.

Leave a Reply

Your email address will not be published. Required fields are marked *