Strategies-Peter Lynch PEG Strategic Investment in Quantitative Class

Strategies-Peter Lynch PEG Strategic Investment in Quantitative Class

I believe that many students who have bought stocks should have heard of the PEG valuation stock selection method. This strategy is strongly promoted by the legendary American fund manager Peter Lynch.

Let’s first introduce Peter Lynch. During the 13 years from 1977 to 1990, the Magellan fund he managed at Fidelity Inc. grew from 20 million U.S. dollars to 14 billion U.S. dollars, with an average compound interest rate of 29% in 13 years. (If you just compare the rate of return during his career, he even surpassed Father Buffett.) At the peak of his career, he chose to retire. Like Jobs, their legends stay in the most exciting places.

When Peter Lynch was young, he went to the golf course as a caddy, because he felt that it was a place where the rich gathered. He wanted to get a deeper understanding of the rich and squeeze into the circle of the rich. And he was able to enter the Fidelity company for an internship. When I was a caddy, I met the manager of Fidelity. This has been imitated by many people. Li Ka-shing's second son, Li Zekai, also worked as a caddy on the golf course when he was young.

Peter Lynch bought more than 15,000 stocks, which has become a topic of ridicule by many people. There was even a question on a program in the United States: What stocks did Peter Lynch have not bought?

It's a bit farther away. Let's go back to PEG. Peter Lynch's thesis is: If any company's stock is priced reasonably, the price-earnings ratio will be equal to the earnings growth rate. This sentence is also the famous PEG valuation method, so what does PEG mean? Let me add here to Mr. Buffett’s point of view. Mr. Ba believes that the annual long-term return of a company’s stock is equal to the reciprocal of the company’s ROE. So which factor is more related to the growth rate? I believe everyone will know Your own judgment. In this article, we believe that what Peter Lynch said is correct, and use the PEG strategy as a backtest.

First of all, introduce a few basic concepts, I believe investors are already familiar with it.

  • EPS (Earnings Per Share) means earnings per share:

To put it simply, it is the net profit divided by the number of shares issued, which is how much money can be made per share on average.

  • PE (Price to Earning Ratio) represents the price-to-earning ratio, which is the ratio of the current stock price P to the earnings per share EPS:

There are many kinds of EPS algorithms, which leads to many kinds of PE:

  1. Rolling price-earnings ratio (TTM): Earnings per share (EPS) is the average of the earnings per share of the last 4 quarterly reports
  2. Static price-to-earnings ratio (LYR): Earnings per share (EPS) takes the earnings per share of last year
  3. Dynamic price-earnings ratio: Earnings per share (EPS) takes the forecast earnings per share for the next year

Here, we take the rolling price-earnings ratio, which is more accurate. Because the static price-earnings ratio is calculated based on last year's EPS, the time difference may be a little bit, while the dynamic price-earnings ratio is the predicted value. Who can tell the future?

  • G (Growth Rate of Expected Profit) represents the company's revenue growth rate

There are also many formulas for calculating the growth rate of earnings. Because of the growth rate, you can calculate both the growth rate of net profit and the growth rate of EPS.

So G can be written as

Can also be written as

After introducing the above concepts, we can introduce the PEG formula

Let's take a simple example to talk about the above indicators for easy understanding by novice users. If you are a veteran of the stock market, you can skip this paragraph. Saying that Xiao Ming has a listed company that sells steamed buns. Last year’s net profit was 100 million, and a total of 10 million shares were issued. The current stock price is 50/share, so EPS=1 billion/10 million=10, PE= 50/10=5; due to the newly opened office building next to the company, the number of people coming to eat buns suddenly increased, the net profit became 120 million, and the stock price rose to 60, so this year’s EPS=120 million/10 million=12, PE=60/12=5. The growth rate G=(1.2-1)/1=20%, PEG=5/(0.2*100)=0.25.

The higher the PEG, the larger the PE or the smaller the G, indicating that the company’s stock price is overvalued, or that the company’s performance growth is too slow, and it is not recommended to buy; on the contrary, if the PEG is smaller, it means that the PE is smaller or the G is smaller. Large, indicating that the company's stock price is undervalued, or that the company gains faster, you can consider buying.

Scope of application of PEG valuation method

Almost every strategy has its scope of application, and the PEG valuation method does not apply to the following two situations:

  • Cyclical industry
  • Financing dependent enterprise
  • Project dependent enterprise

The reason is also very simple. The profitability of cyclical industry companies may not be due to the good development of the company itself, but the industry cycle. At this time, the growth rate may be very high, but it is not sustainable; the second and third types are more obvious. , It’s even more unsuitable for companies that have had a last meal without a meal. Cyclical industries are easy to judge, such as coal and steel; but the second and third types need to be found in financial reports. Like the example we gave above, the 20% increase in performance was due to the opening of a new office building nearby and the increase in people buying buns. This is unsustainable.

The following table lists the relationship between PEG range and stock valuation.

PEG

Stock valuation

0~0.5

Relatively underestimated

0.5~1

Relatively reasonable

1~2

Relatively overestimated

>2

high risk

Using PEG strategy for backtesting, the idea is as follows:

  • Adjust the warehouse every N days
  • For each position adjustment, select the M stocks with the smallest PEG in the CSI 300 and put them into the stock pool
  • Sell ​​stocks that are not in the stock pool this time, and buy stocks that are newly added to the stock pool

The results of the backtest are as follows:

It can be seen that if the parameters are appropriate, the return of the PEG strategy is much greater than the average return of the market. If we can also judge the obvious high in the market (for example, 6000 points in 2007, 5000 points in 2015), it is far from the relatively high point. Field, then our profits will be even higher. After all, in a bear market like A-shares where you can only make money by doing long, no matter how good the strategy is, it is ineffective.

The main difficulty of PEG is not in PE, but in G. Its core is that we have to discover potential industries or companies. This requires other means to assist us in our judgment; we will talk about these in later articles.

In order to avoid the suspicion of advertising on a certain quantitative platform, here we present the backtest procedures and results on the two platforms jointquant and uqer.

jointquant: (Note: This program is an example program in the quantization class on the official website of joinquant)

import pandas as pd

'''
================================================== ===============================
Before the overall backtest
================================================== ===============================
'''
#Overall things to do before backtesting
def initialize(context):
    set_params() # set strategy constant
    set_variables() # Set intermediate variables
    set_backtest() # Set backtest conditions

#1 
#Set strategy parameters
def set_params():
    g.tc = 15 # Adjust the number of days
    g.num_stocks = 10 # The maximum number of stocks selected for each position adjustment

#2
#Set intermediate variables
def set_variables():
    gt = 0 # Record the number of days the backtest runs
    g.if_trade = False # Whether to trade on the day

#3
#Set back test conditions
def set_backtest():
    set_option('use_real_price',True) # trade with real price
    log.set_level('order','error') # Set the error level

'''
================================================== ===============================
Before the market opens every day
================================================== ===============================
'''
#Things to do before opening every day
def before_trading_start(context):
    if gt%g.tc==0:
        g.if_trade=True # Every g.tc days, adjust the position once
        set_slip_fee(context) # Set handling fee and handling fee
        g.stocks=get_index_stocks('000300.XSHG') # Set CSI 300 as the initial stock pool
        # Set up viable stock pool
        g.feasible_stocks = set_feasible_stocks(g.stocks,context)
    g.t+=1


#4
# Set up a viable stock pool: filter out the stocks that are suspended on the day
# Input: initial_stocks is a list type, which means the initial stock pool; context (see API)
# Output: unsuspened_stocks is a list type, indicating the stock pool that has not been suspended on the day, that is: feasible stock pool
def set_feasible_stocks(initial_stocks,context):
    # Determine whether the stocks in the initial stock pool are suspended, and return to the list
    paused_info = []
    current_data = get_current_data()
    for i in initial_stocks:
        paused_info.append(current_data[i].paused)
    df_paused_info = pd.DataFrame({'paused_info':paused_info},index = initial_stocks)
    unsuspened_stocks =list(df_paused_info.index[df_paused_info.paused_info == False])
    return unsuspened_stocks


#5
# Set slippage and handling fee according to different time periods
# Input: context (see API)
# Output: none
def set_slip_fee(context):
    # Set slippage to 0
    set_slippage(FixedSlippage(0)) 
    # Set the handling fee according to different time periods
    dt=context.current_dt
    if dt>datetime.datetime(2013,1, 1):
        set_commission(PerTrade(buy_cost=0.0003, sell_cost=0.0013, min_cost=5)) 

    elif dt>datetime.datetime(2011,1, 1):
        set_commission(PerTrade(buy_cost=0.001, sell_cost=0.002, min_cost=5))

    elif dt>datetime.datetime(2009,1, 1):
        set_commission(PerTrade(buy_cost=0.002, sell_cost=0.003, min_cost=5))
    else:
        set_commission(PerTrade(buy_cost=0.003, sell_cost=0.004, min_cost=5))




'''
================================================== ===============================
When trading every day
================================================== ===============================
'''
# What to do during the backtest every day
def handle_data(context,data):
    if g.if_trade == True:
        # G.num_stocks stocks to be bought, list type
        list_to_buy = stocks_to_buy(context)
        # Stocks to be sold, list type
        list_to_sell = stocks_to_sell(context, list_to_buy)
        # Sell operation
        sell_operation(list_to_sell)
        # Buy operation
        buy_operation(context, list_to_buy)
    g.if_trade = False

#6
# Calculate the PEG value of the stock
# Input: context (see API); stock_list is a list type, which means stock pool
# Output: df_PEG is dataframe: index is the stock code, data is the corresponding PEG value
def get_PEG(context, stock_list): 
    # Query the price-earnings ratio and earnings growth rate of stocks in the stock pool
    q_PE_G = query(valuation.code, valuation.pe_ratio, indicator.inc_net_profit_year_on_year
                 ).filter(valuation.code.in_(stock_list)) 
    # Get a dataframe: include stock code, price-earnings ratio PE, income growth rate G
    # Default date = the day before context.current_dt, use the default value to avoid future functions, it is not recommended to modify
    df_PE_G = get_fundamentals(q_PE_G)
    # Screen out growth stocks: delete stocks with negative price-to-earnings ratios or earnings growth rates
    df_Growth_PE_G = df_PE_G[(df_PE_G.pe_ratio >0)&(df_PE_G.pe_ratio <80)/
                            &(df_PE_G.inc_net_profit_year_on_year >0)&(df_PE_G.inc_net_profit_year_on_year <200)]
    # Remove the row of stocks whose PE or G values ​​are not numbers
    df_Growth_PE_G.dropna()
    # Get a Series: the price-earnings ratio TTM of stocks, that is, the PE value
    Series_PE = df_Growth_PE_G.ix[:,'pe_ratio']
    # Get a Series: the growth rate of the stock’s income, that is, the G value
    Series_G = df_Growth_PE_G.ix[:,'inc_net_profit_year_on_year']
    # Get a Series: store the PEG value of the stock
    Series_PEG = Series_PE/Series_G
    # Match the stock to its PEG value
    Series_PEG.index = df_Growth_PE_G.ix[:,0]
    # Convert Series type to dataframe type
    df_PEG = pd.DataFrame(Series_PEG)
    return df_PEG

#7
# Get buy signal
# Input: context (see API)
# Output: list_to_buy is a list type, which means g.num_stocks stocks to be bought
def stocks_to_buy(context):
    list_to_buy = []
    # Get a dataframe: index is the stock code, data is the corresponding PEG value
    df_PEG = get_PEG(context, g.feasible_stocks)
    # Arrange the stocks in ascending order of PEG, and return the daraframe type
    df_sort_PEG = df_PEG.sort(columns=[0], ascending=[1])
    # Convert the stored order stock code index into a list and take the first g.num_stocks as stocks to be bought, and return to the list
    for i in range(g.num_stocks):
        if df_sort_PEG.ix[i,0] <0.5:
            list_to_buy.append(df_sort_PEG.index[i])
    return list_to_buy


#8
# Get a sell signal
# Input: context (see API documentation), list_to_buy is a list type, representing stocks to be bought
# Output: list_to_sell is a list type, indicating stocks to be sold
def stocks_to_sell(context, list_to_buy):
    list_to_sell=[]
    # For stocks that do not need to be held, sell out
    for stock_sell in context.portfolio.positions:
        if stock_sell not in list_to_buy:
            list_to_sell.append(stock_sell)
    return list_to_sell


#9
# Perform sell operation
# Input: list_to_sell is a list type, which means stocks to be sold
# Output: none
def sell_operation(list_to_sell):
    for stock_sell in list_to_sell:
        order_target_value(stock_sell, 0)


#10
# Perform a buy operation
# Input: context (see API); list_to_buy is a list type, indicating the stocks to be bought
# Output: none
def buy_operation(context, list_to_buy):
    for stock_sell in list_to_buy:
        # Allocate funds for each holding stock
        g.capital_unit=context.portfolio.portfolio_value/len(list_to_buy)
        # Buy stocks in the "list of stocks to buy"
        for stock_buy in list_to_buy:
            order_target_value(stock_buy, g.capital_unit)

'''

uqer:

The result of the backtest in uqer is different from the backtest in joinquant, because the following program does not add a viable stock pool, that is, the daily screening of unsuspended stocks, and the proportion of each investment is not the same. You can observe these The backtest difference brought about by two parameters.

# uqer's data does not have the revenue growth rate, that is, G, so the net profit growth rate is used instead
import pandas as pd
import numpy as np
import datetime
# system parameters
start = '2012-01-06' # Backtest start time
end = '2018-01-06' # End time of back test
universe = DynamicUniverse('HS300').apply_filter(Factor.PE.nsmall(100)) # Securities pool, supporting four assets of stocks, funds, futures and indexes
benchmark ='HS300' # Strategy reference standard
freq ='d' # Strategy type,'d' indicates that the daily strategy uses the daily backtest, and'm' indicates the intraday strategy uses the minute backtest
refresh_rate = 15 # Adjust the frequency, which means the time interval for executing handle_data, if freq ='d', the unit of time interval is trading day, if freq ='m', the time interval is minutes

# my parameters
num_stocks = 10 # The maximum number of stocks for each adjustment
capital_unit = 1.0/num_stocks


# Configure account information, support multiple assets and multiple accounts
accounts = {
    'stock_account': AccountConfig(account_type='security', 
                                     capital_base=10000000,
                                    slippage = Slippage(value=0.001, unit='perValue'), # Slippage is set to percentage slippage 0.001
                                    commission = Commission(buycost = 0.0003, sellcost = 0.002, unit ='perValue') #handling fee   
                                    )
}

def initialize(context):
    print'initialize...'

# Call once per unit time (if it is back-tested by day, it will be called once a day, if it is called by minutes, it will be called once every minute)
def handle_data(context):    
    print'handle_data...'
    print context.current_date
    cur_date = context.current_date
    stock_account = context.get_account('stock_account')
    current_universe = context.get_universe('stock', exclude_halt=True) #Feasible stock pool
    list_to_buy = stocks_to_buy(context,current_universe, cur_date)
    # print list_to_buy
    # list_to_sell = stocks_to_sell(context, stock_account, list_to_buy)
    # sell_operation(list_to_buy,stock_account)
    # buy_operation(context,list_to_buy,stock_account)
    current_position = stock_account.get_positions(exclude_halt=True)
    for stock in set(current_position).difference(list_to_buy):
        stock_account.order_to(stock, 0)

    for stock_buy in list_to_buy:
        print stock_buy
        stock_account.order(stock_buy, 10000)#(stock_buy,capital_unit)

def get_PEG(context, current_universe, cur_date):
    # Get the data of PE and G
    df_PE_G = DataAPI.MktStockFactorsOneDayGet(tradeDate=cur_date,secID=current_universe,ticker='',field=['secID','PE','NetProfitGrowRate'],pandas="1")
    df_Growth_PE_G = df_PE_G[(df_PE_G['PE']>0) & (df_PE_G['NetProfitGrowRate']>0)]
    # print df_Growth_PE_G
    df_Growth_PE_G.dropna()
    Serial_PE = df_Growth_PE_G.loc[:,'PE']
    Serial_G = df_Growth_PE_G.loc[:,'NetProfitGrowRate']
    Serial_PEG = Serial_PE/(100 * Serial_G)
    # print Serial_PEG
    Serial_PEG.index = df_Growth_PE_G.iloc[:,0]
    df_PEG = pd.DataFrame(Serial_PEG)
    # print('get PEG done')
    return df_PEG

def stocks_to_buy(context,current_universe, cur_data):
    list_to_buy = []
    # print current_universe
    # print cur_data
    df_PEG = get_PEG(context,current_universe, cur_data)
    # print df_PEG
    df_sort_PEG = df_PEG.sort(columns = [0], ascending = [1]) #ascending
    # print df_sort_PEG
    # Select num_stocks stocks
    for i in range(num_stocks):
        if df_sort_PEG.iloc[i,0] <0.5: # PEG--0.5:
            list_to_buy.append(df_sort_PEG.index[i])
    return list_to_buy

def stocks_to_sell(context, stock_account, list_to_buy):
    list_to_sell = []
    # If it is not in the stock pool to be bought, it is the stock to be sold
    for stock_sell in stock_account.get_positions().keys():
        if stock_sell not in list_to_buy:
            list_to_sell.append(stock_sell)
    return list_to_sell

def sell_operation(list_to_buy,stock_account):
    for stock in set(stock_account.get_positions(exclude_halt=True)).difference(set(list_to_buy)):
        stock_account.order_to(stock, 0)

def buy_operation(context,list_to_buy,stock_account):
    for stock_buy in list_to_buy:
        print stock_buy
        # stock_account.order_pct(stock_buy,capital_unit)
        stock_account.order(stock_buy, 10000)
Reference: https://cloud.tencent.com/developer/article/1652981 Strategies-Peter Lynch PEG Strategic Investment in Quantitative Classes-Cloud + Community-Tencent Cloud