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.
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.
There are many kinds of EPS algorithms, which leads to many kinds of PE:
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?
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:
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:
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)