GPN Stock Study (2-13-23)
Posted by Mark on December 16, 2022 at 07:05 | Last modified: March 8, 2023 14:08I recently* did a stock study on Global Payments Inc. (GPN) with a closing price of $117.88.
CFRA writes:
> Global Payments (GPN) provides payment processing and software
> solutions globally through a variety of distribution channels,
> which enable customers to accept card, electronic, check, and
> digital-based payments. Specific offerings include
> authorization, settlement, funding, customer support,
> chargeback, security, and billing services.
This medium-sized company has grown sales and earnings at annualized rates of 14.5% and 6.5% over the last 10 years, respectively. Visual inspection is not great. Revenue data is [missing for ’16 and] down in ’18 while EPS data is [missing for ’16 and] down in ’15, ’18, ’19, and ’20. Q2 and Q3 2022 EPS have declined sharply. [Stock] Price bars in ’17 and ’22 overlap, which represents several years without significant appreciation. PTPM, which averaged 14.6% from ’12-’14, has averaged 11.5% over the last five years. Although apparently in decline, this beats peer and industry averages.
ROE has averaged 4% over the last five years, which seems low. The industry averages 6%, though, while peers average 6.8% (both excluding ’20, which is -161% or worse). Debt-to-Capital, which ranged from 51.1% (’12) to 75.5% (’14) between ’12-’18, has averaged 28% over the last three years. The latter is less than peer and industry averages. Quick Ratio and Interest Coverage are a concerning 0.53 and 1.4, respectively.
The fact that so many analysts are covering this company gives me some [hopefully not illusory] assurance about its liquidity. Aside from all the analysts represented below, M* says the balance sheet is sound. Value Line gives a B++ financial strength grade and says “it should continue to easily meet its various obligations.” CFRA says expected FCF generation ($2.2B and $2.5B in ’22 and ’23, respectively) should allow leverage to return to historical levels by the end of ’23.
I assume long-term annualized sales growth of 8% based on the following:
- CNN Business projects 4.9% YOY growth and 6.6% per year for ’22 and ’21-’23, respectively (based on 27 analysts).
- YF projects YOY 6.3% and 9.2% growth for ’23 and ’24, respectively (24 analysts).
- Zacks projects YOY growth of 6.6% and 2.6% for ’23 and ’24, respectively (7).
- Value Line projects annualized growth of 8.3% from ’21-’26.
- CFRA projects 3.9% YOY contraction for ’23.
- M* provides a 2-year ACE of 1.6% growth and 8% growth per year through ’26 (analyst note).
>
I assume long-term annualized EPS growth of 10% based on the following:
- CNN Business projects 10.4% YOY and 12.5% per year for ’22 and ’21-’23, respectively (based on 27 analysts), along with 5-year annualized growth of 14.8%.
- MarketWatch projects 12.5% and 11.5% per year for ’22-’24 and ’22-’25, respectively (31 analysts).
- Nasdaq.com projects 11.8% YOY growth for ’24 (10 and 2 analysts for ’23 and ’24).
- Seeking Alpha projects 4-year annualized growth of 17%.
- YF projects YOY 11.4% and 14.5% for ’23 and ’24, respectively (24), along with 5-year annualized growth of 15.1%.
- Zacks projects YOY 9.9% and 13.6% for ’23 and ’24, respectively (7), along with 5-year annualized growth of 15.8%.
- Value Line projects annualized growth of 13.7% from ’21-’26.
- CFRA projects 14.5% YOY for ’23 and 3-year projected annualized growth of 15%.
- M* provides a long-term estimate of 16.6%.
>
My forecast is below the range of six long-term estimates (mean 15.5%).
As Q2 and Q3 of ’22 were atypically soft ($0.13 and $0.17/share, respectively), I will not originate the earnings projection from there. Given trendline ($2.90) or ’21 ($3.29), I choose the former (more conservative).
My Forecast High P/E is 35. Over the last 10 years, high P/E has trended up from 18.6 (’12) to 67.1 (’21) excluding upside outliers of 85.8 and 110.6 in ’19 and ’20. The last-5-year average (excluding outliers) is 49.2.
My Forecast Low P/E is 28. Over the last 10 years, low P/E has trended up from 14.3 (’12) to 35.5 (’21). ’19 and ’20 were high at 45.6 and 54.1, but not to an extreme. The last-5-year average is 38.3.
My Low Stock Price Forecast is $92.10. In projecting from the trendline, I also need to manually override EPS (to $2.90) rather than defaulting to TTM ($0.17) for calculation of the Low Stock Price Forecast. This is 21.9% below previous close, equal to the 52-week low, and below both ’20 and ’21 low stock prices.
Payout Ratio was below 3% in ’18 and earlier before averaging 25.8% over the last three years. I will assume 10% to be conservative.
All this computes to an U/D ratio of 2.6, which makes GPN a HOLD. The Total Annualized Return is 9.8%, but PAR (using Forecast Average, not High, P/E) is 7.5%: less than I want out of a medium-sized company.
To better interpret this, I use Member Sentiment (MS) to assess the study’s margin of safety (MOS). Out of 50 studies over the past 90 days (my own and two others with invalid low prices excluded), projected sales, projected EPS, Forecast High P/E, Forecast Low P/E, and Payout Ratio average 9.3%, 22.2%, 44.4, 29, and 13.9%, respectively. My inputs are all lower. Value Line projects an average annual P/E of 40, which is higher than MS (36.7) and me (31.5). MOS seems robust here.
MS has an extreme Low Stock Price Forecast of $63.83. This is over 30% lower than mine and below the 2017 low price. Eleven studies are in the single digits, which leaves me scratching my head. From the lowest price in the last three months to now, GPN has rallied 26.5%. This may explain some of the forecasts, but not those under $30/share.
Excluding these 15 studies, I get an MS Low Stock Price Forecast of $87.42, which seems more reasonable. This would equate to a Forecast Low P/E of 26.5 in my study. I will stick with my $92.10 because my forecast P/E range is low enough already.
>
*—Publishing in arrears as I’ve been doing one daily stock study while posting only two blogs per week.
Backtester Development (Part 8)
Posted by Mark on December 13, 2022 at 07:02 | Last modified: June 22, 2022 08:37Today I will continue (see end of Part 7) with backtester logic for maximum excursion (ME).
I will begin talking about the output files. Two dataframes are created with pd.DataFrame(): btstats for intratrade monitoring and summary_results for end-of-trade reporting. These are ultimately converted to .csv files with pd.to_csv().
Rows are added sequentially to the dataframes upon completion. For btstats, add_btstats is created where each list element corresponds to a dataframe column. Minimal calculation in this list statement (e.g. L_iv_orig x 100) could have been done with variable assignment if I renamed the variables to reflect it. Index of the last record is then used to add list to the bottom:
> btstats.loc [ len ( btstats . index ) ] = add_btstats
In contrast to adding rows as lists, I build summary_results by adding rows as one dictionary per trade:
> summary_results = summary_results . append ( { ‘Trade Num’ : len (trade_list) }, ignore_index = True)
This is a long line with a key-value pair for every column (only one of which is shown here). The final argument is needed when appending a dictionary to avoid a TypeError.
Pandas documentation says DataFrame.append() has been deprecated in favor of .concat(). I do not get any such warning. A closer look suggests the deprecation pertains to adding one dataframe to another. I am adding a dictionary.
Now that I have described the structures in which ME will be reported, let’s talk about ME itself. By definition, MFE (favorable) is the farthest a trade goes in my favor before ending up a loser and MAE (adverse) is the farthest a trade goes against me before ending up a winner. Strictly speaking, a losing (winning) trade has no MAE (MFE).
However, a useful application might involve running trades from start to finish without profit targets or max losses to record largest intratrade loss and gain. Plotting the two against each other can then give an idea whether a particular stop level might lock in more winners to the exclusion of losers or avoid locking in more losers to the exclusion of winners. All this is contingent on knowing whether intratrade gain or loss comes first. I have the _dte variables to tell me that.
To allow for such application, I define MFE (MAE) to be maximum intratrade gain (loss) without regard to trade outcome. My tweak is to recognize MFE (MAE) for winning (losing) trades as the previous day’s MFE (MAE) before stop level is hit.
Needing to differentially apply current or previous ME when stop levels are checked after ME is updated makes the program logic more complicated. For trade_status ‘IN_TRADE,’ I report MAE and MFE. For ‘WINNER’ (‘LOSER’), I report MAE and MFE_prev (MAE_prev and MFE). All this leaves me with three different add_btstats lines that need to be properly fit into an if-elif-else block. The same goes for summary_results.
I will continue next time.
Categories: Python | Comments (0) | PermalinkTGT Stock Study (2-12-23)
Posted by Mark on December 8, 2022 at 06:43 | Last modified: March 8, 2023 10:36I recently* did a stock study on Target Corp. (TGT) with a closing price of $170.02.
Value Line writes:
> Target Corp.’s operations consisted of 1,926 discount stores,
> of which 1,528 were owned, in the U.S., mostly in Cal., Tex.,
> and Fla (as of 1/29/22)… Sales by category in fiscal ’21:
> beauty/household, 26%; hardlines, 18%; apparel/accessories,
> 17%; food, 20%; and home furnish., 19%.
This mega-sized company (annual revenue > $50B) has grown sales and earnings 3.4% and 13.1% per year over the last 10 years, respectively. Lines are mostly up except for dips in sales (’16) and EPS (’13 and ’16). PTPM over the last decade has ranged from 4.3% (’14) to 8.4% (’21) with a last-5-year average of 5.9%. This is higher than peer and industry averages.
ROE has trended up over the last 10 years from 18% (’12) to 32.5% in ’20 (’21 was an upside outlier at 48.1%) with a last 5-year average (excluding ’21) of 28.1%. This is higher than peer and industry averages. Debt-to-Capital has increased from 49.4% (’12) to 56.2% (’21) with a last-5-year average of 53.1%. This is also higher than peer and industry averages. Current Ratio is a suboptimal 0.86, but Interest Coverage is 10.8. M* assigns an Exemplary capital allocation rating while Value Line gives a B++ grade for financial strength.
I assume long-term annualized sales growth of 2% based on the following:
- CNN Business projects 2.5% YOY and 2.2% per year for ’22 and ’21-’23, respectively (based on 31 analysts).
- YF projects YOY 2.3% and 2.2% for ’23 and ’24, respectively (29 analysts).
- Zacks projects YOY 2.3% and 2.8% for ’23 and ’24, respectively (12).
- Value Line projects 3.8% annualized growth from ’21-’26 (11% from ’20-’26).
- CFRA projects growth of 2.8% YOY and 2.3% per year for ’23 and ’22-’24, respectively.
- M* provides a 2-year ACE of 2.1% and a 10-year estimate of 3% in its analyst note.
>
My estimate is just below all of the above.
I assume long-term annualized EPS growth of 2% based on the following:
- CNN Business projects a 59.3% YOY contraction and 17% contraction per year for ’22 and ’21-’23, respectively (based on 31 analysts), along with 5-year annualized contraction of 4.2%.
- MarketWatch projects annualized contraction of 15.9% and 5.5% for ’22-’24 and ’22-’25, respectively (36 analysts).
- Nasdaq.com projects annualized growth of 42.6% and 36.5% for ’23-’25 and ’23-’26, respectively [16, 10, and 2 analysts for ’23, ’25, and ’26].
- Seeking Alpha projects 5-year annualized growth of 10.5%.
- YF projects 59.1% YOY contraction and 70% YOY growth for ’23 and ’24, respectively (33), along with 5-year annualized contraction of 4.9%.
- Zacks projects 59.2% YOY contraction and 71.4% YOY growth for ’23 and ’24, respectively (16), along with 5-year annualized growth of 9.9%.
- Value Line projects annualized growth of 5.8% from ’21-’26.
- CFRA projects 56.8% YOY contraction and 15.9% contraction per year for ’23 and ’22-’24, respectively, and 1% growth per year from ’21-‘24%.
- M* projects long-term annualized growth of 4.8%.
>
I am projecting below the average [of six] long-term estimate[s] (3.6%).
I will override to project earnings from the last annual data point rather than Q3 ’22. Analyst consensus expects a big rebound in earnings for ’23, which is suggestive of a one-year anomaly due to the macroeconomic factors rather than maturation in business cycle. The latter has already occurred for this company.
The ideal situation would be to project from the same point analysts are using for their calculations, but because this is not explicitly disclosed, I have to do my best to work around it. Here, projection from the last quarterly data point or EPS trendline makes no sense because Forecast High Stock Price would be at or below the current stock price.
My Forecast High P/E is 17. Over the last 10 years, high P/E has ranged from 14.6 (’12) to 23.9 (’13) with a last-5-year average of 18.8.
My Forecast Low P/E is 10. Over the last 10 years low P/E has ranged from 9.1 (’17) to 18.2 (’13). The last-5-year average is 10.6. As suggested above, I think Target is a mature company so I don’t expect the kind of P/E compression that might be seen with a younger, more explosive growth company.
I’ve realized that I need to be very careful with the website and future EPS projection. Overriding the base on the estimated EPS growth rate screen leaves TTM EPS for the Low Stock Price Forecast. The latter may require changing as well.
My Low Stock Price Forecast is $137. The default value would be $141. This corresponds to ’21 earnings of $14.10/share and assumes 0% growth for the next five years. With the 52-week low price at $137.2, I’m overriding with $13.70/share to [roughly] match. $137 is 19.4% below the previous close.
Over the last 10 years, Payout Ratio has ranged from 22.4% (’21) to 51.5% (’13). The last-5-year average is 37.2%. I am assuming 31%.
All this computes to an U/D ratio of 2.9, which makes TGT a HOLD. The Total Annualized Return is 11.1%, and PAR (using Forecast Average, not High, P/E) is 6.6%, which is less than desired. Can I convince myself that the inherent margin of safety (MOS) in this study allows for a good chance of realizing Forecast High P/E?
To answer this, I compare my inputs with the larger sample size of Member Sentiment (MS). Out of 192 studies over the past 90 days (my own and four others with projected low prices above last closing price excluded), projected sales, projected EPS, Forecast High P/E, Forecast Low P/E, and Payout Ratio average 4.7%, 7.8%, 18.4, 11.3, and 38.4%, respectively. My inputs—especially the growth rates—are all lower. Value Line projects an average annual P/E of 15, which is barely higher than MS (14.9) and higher than mine (13.5). All this is indicative of a healthy MOS.
MS provides a[n] [average] Low Stock Price Forecast of $97.58, which is 28.8% lower than mine. It’s also closer to the 2020 [COVID crash] low of $90.20. In my opinion, a stock price five years from now > 20% below today’s price is almost unfathomable for [even] a[n] [anemic] growth company. This legitimizes my Low Stock Price Forecast. Forecasting close to the ’20 lows almost seems preposterous. I’d be curious to know if these MS studies were accompanied by a manual EPS override.
A significantly lower Low Stock Price Forecast would move TGT further into the Hold zone, but because this seems unreasonable I choose to ignore it as an offset to the MOS. The only remaining question in my mind is why CNN Business (FactSet) and YF (Refinitiv) project negative 5-year earnings growth when the other four do not.
That concern aside, I feel comfortable with TGT as a BUY under $169/share.
>
*—Publishing in arrears as I’ve been doing one daily stock study while posting only two blogs per week.
Backtester Development (Part 7)
Posted by Mark on December 5, 2022 at 07:16 | Last modified: June 22, 2022 08:37Today I will finish discussing the ‘find_spread’ control branch before moving on to ‘update_long.’
A few final steps are taken after the spread is identified:
- All variables are assigned from lists.
- First row of btstats (intratrade_results) is added.
- trade_status is changed to IN_TRADE.
- Control flag gets assigned ‘update_long.’
- Several variables and lists are reset. This includes converted_date.
- Break out of the for loop.
>
The last step is critical. I initially included a continue statement, which repeats the loop and selects the longest-dated option under 200 DTE every time: definitely not what I want.
I’m somewhat confused in determining where the program goes next, but I think it must be back to the top of the data file iteration loop. The for loop, of which this is a part, concludes the ELSE of the ‘find_spread’ control branch. Unlike the previous version, at this point the program is already looking at the next date so nothing needs to be done with wait_until_next_day.
The ‘update_long’ branch is brief. If strike price and long expiration date match, then variables for the long option are updated along with underlying price. If strike price and expiration date do not match, then continue to the next line of the data file.
I am sloppy with what variables to reset at the end of ‘find_spread.’ Some variables not reset are used in ‘update_long.’ What matters most is that every variable to be subsequently passed to btstats gets assigned a new value as part of the update branches. I’ve discussed possibly using functions to initialize and reset variables. Much of the resetting (and initializing) is unnecessary as long as I assign everything at the proper points. I try and reset where I can since I don’t trust myself with this, but I could shorten the program simply by being more careful.
The ELSE, which executes when control_flag is ‘update_short,’ begins with a check to make sure current_date still matches historical date. False would indicate the spread failed to be updated: a fatal flaw.
Next and similar to above, if strike price and short expiration date match, then variables for the short option and the spread are updated. No need to change underlying price as date has not changed.
I then include logic for max adverse/favorable excursion. I need variables for MFE, MAE, MFE_dte, and MAE_dte. I also need to store the previous values because I never want the max excursion to be equal to closing PnL.* If ROI_current < MAE (or > MFE), then the MAE (MFE) values get assigned to a _prev variable set and ROI_current gets assigned to MAE (MFE).
I will continue next time.
>
*—Alternatively, I could have used and maintained lists with two elements
each rather than duplicating the variable set with a _prev suffix.
ULTA Stock Study (2-11-23)
Posted by Mark on December 1, 2022 at 07:23 | Last modified: July 18, 2023 13:49I recently* did a stock study on Ulta Beauty, Inc. (ULTA) with a closing price of $515.75.
M* writes:
> With roughly 1,350 stores and a partnership with narrow-moat
> Target, Ulta Beauty is the largest specialized beauty retailer
> in the U.S. The firm offers makeup (43% of 2021 sales),
> fragrances, skin care, and hair care products (20% of 2021
> sales), and bath and body items. Ulta offers private-label
> products and merchandise from more than 500 vendors. It also
> offers salon services, including hair, makeup, skin, and brow
> services, in all stores. Most Ulta stores are approximately
> 10,000 square feet and are in suburban strip centers.
This medium-sized company has grown sales and earnings at annualized rates of 15.7% and 16.6% over the last 10 years, respectively. The stock price has hardly seen a reprieve. Lines are mostly up, straight, and parallel except for an EPS decline in ’20. PTPM over the last decade has increased from 12.6% to 15% with a last-5-year average (excluding ’20, which was a downside outlier) of 13.3%. This is higher than BBWI (a peer) and industry averages.
ROE has increased from 24.2% to 47.3% over the last 10 years with a last-5-year average (excluding ’20) of 38.5%. This outpaces BBWI and industry averages. Debt-to-Capital was 0% until 2019 and has averaged 51.3% over the last three years (lower than BBWI and industry averages). This is all uncapitalized leases as the company has zero long-term debt.
I assume long-term annualized sales growth of 7% based on the following:
- CNN Business projects 16.3% YOY and 11.5% per year for ’22 and ’21-’23, respectively (based on 24 analysts).
- YF projects YOY 15.7% and 7.3% for ’23 and ’24, respectively (28 analysts).
- Zacks projects YOY 15.8% and 7.8% for ’23 and ’24, respectively (10).
- Value Line projects 4.4% annualized growth from ’21-’26 (12.5% from ’20-’26).
- CFRA projects growth of 15.6% YOY and 10.1% per year for ’23 and ’22-’24, respectively.
- M* provides a 2-year ACE of 12.1% and a 10-year estimate of 7.5% (analyst note).
>
I assume long-term annualized EPS growth of 8% based on the following:
- CNN Business projects 28.2% YOY and 16.3% per year for ’22 and ’21-’23, respectively (based on 24 analysts), along with a 5-year annualized growth of 12.7%.
- MarketWatch projects annualized growth of 18.9% and 14.8% for ’22-’24 and ’22-’25, respectively (29 analysts).
- Nasdaq.com projects 7.2% and 7.9% growth per year for ’23-’25 and ’23-’26, respectively [14, 8, and 1 analyst(s)].
- Seeking Alpha projects 4-year annualized growth of 23.9%.
- YF projects YOY 27.3% and 5.6% for ’23 and ’24, respectively (30), along with 5-year annualized growth of 12%.
- Zacks projects 5.6% YOY for ’24 and a 5-year annualized growth of 13.8% (14).
- Value Line projects 8.9% annualized from ’21-’26.
- CFRA projects 26.1% YOY and 12.8% per year for ’23 and ’22-’24, respectively, along with 23% per year from ’21-’24.
- M* provides a long-term estimate of 11%.
>
I am projecting well below the average [of seven] long-term estimate[s] (15%).
My Forecast High P/E is 23. Over the last 10 years, high P/E has ranged from 20 (’21) to 42.1 (’13) excluding the upside outlier in ’20 (99.8). The last-5-year average is 29.6 (excluding ’20). This has been trending lower.
My Forecast Low P/E is 15. Excluding the upside outlier in ’20 (39.9), over the last 10 years low P/E has trended down from 28.4 to 15.8. The last-5-year average (excluding ’20) is 18.1.
My Low Stock Price Forecast is the default value of $341.30. The 52-week low price is $330.80, which makes this reasonable despite being 33.8% below the last closing price. The stock has been on a tear lately.
All this computes to an U/D ratio of 0.9, which makes ULTA a HOLD. The Total Annualized Return is 5.3%, and PAR (using Forecast Average, not High, P/E) is 2.6%: less than the current yield on T-Bills.
To assess margin of safety (MOS) in this study, I compare my inputs with Member Sentiment (MS). Out of 536 studies over the past 90 days (my own and eight others with projected low prices above last closing price excluded), projected sales growth, projected EPS growth, Forecast High P/E, and Forecast Low P/E average 10.7%, 10.7%, 29.5, and 20.8, respectively. My inputs are all lower. Value Line projects an average P/E of 22, which is lower than MS (25.1) and higher than mine (19).
MS has a Forecast Low Stock Price of $287.67, which is ~15% below mine. This is no surprise given that the stock has rallied ~20% over the last three months; for many studies, this was calculated when the stock was lower.
Despite a decent MOS, I await prices under $423/share to revisit ULTA.
>
*—Publishing in arrears as I’ve been doing one daily stock study while posting only two blogs per week.
Backtester Development (Part 6)
Posted by Mark on November 29, 2022 at 07:26 | Last modified: June 22, 2022 08:36Today I will continue discussion of logic for my revamped backtesting program.
The ‘find_spread’ control branch involves one of four paths:
- If DTE > 200 then continue.
- Elif dte_list empty, strike price multiple of 25, and strike price > underlying price by < 26, then store current_date and append option data to respective lists. At the end, the program will select two options for the spread based on available DTE. Since the iteration cannot go in reverse, potentially relevant data must be stored when encountered.
- Elif DTE matches previously-stored DTE then continue (implied is that the lists are no longer empty).
- Elif current_date still matches, then insert option data to beginning of respective lists (implied is that DTE has changed).
- Else update current_date and proceed with spread selection since iteration of all options from previous date is complete.
>
Motivation for requiring 25-point spreads relates to the fact that 5- and 10-point strikes were not available many years ago when option trading volume was much lower.* Even today, 25-point strikes are regarded as having the most volume and best liquidity. If true (this would be exceedingly difficult to test and may require huge amounts of capital to attain large sample sizes of live trades), then this criterion makes good sense.
Requiring 25-point spreads is not without controversy, however. If I include a penalty for slippage regardless of strike-price multiple, then I probably do not need the 25-point limitation. I think a bigger problem is nonuniform strike density, which I discussed in the fourth paragraph here.
Spread selection proceeds as a nested for loop over dte_list, which now has DTE values of matched strike-price options from the same historical date across increasingly farther-out expirations:
- First list position with value > (30 x mte) is the target short option; all related option data corresponding to this list position are now encoded as such.
- Iterate over remainder of dte_list to match first index position with value greater than S_dte + (27 x width)** as target long option; all related option data corresponding to this list position are now encoded as such.
>
These lists are my solution to the problem of dynamic variables described in Part 4. Rather than initializing multiple sets of variables to hold unnecessary data, data is stored in lists with each element corresponding to a particular DTE option. Once the proper DTEs are determined, corresponding values are then assigned to the only set of pre-initialized variables.
I will continue next time with the ‘update_long’ control branch.
>
*—2017 – 2021 option volume is shown here.
>
**—I am using 27 rather than 28 in case of a Friday holiday.
Generator Expressions and Iterators in Python
Posted by Mark on November 23, 2022 at 06:33 | Last modified: June 2, 2022 14:08After doing some further research, I have learned that my previous explanation of initializing multiple variables as empty lists was not very Pythonic. I want to correct some of that today.
The following is a generator expression:
( [] for i in range(3) )
The expression will generate three empty lists. Parentheses are needed around generator expressions like brackets are needed around list comprehensions:
A generator is a function that returns an object also known as an iterator.
An iterator is an object that contains a countable number of values. An iterator may be iterated over one value at a time until all values have been exhausted. Trying to iterate further will then raise a StopIteration exception (i.e. error or traceback).
A generator expression is like a list comprehension except the latter produces the entire list while the former produces one item at a time. A generator expression has “lazy execution” (producing items only when asked for), which makes it more memory efficient than a list comprehension.
Although the end result is to initialize three variables as empty lists, Pythonically speaking this is doing something else:
list_a, list_b, list_c = ( [] for i in range(3) )
The right side is an object containing three items: [], [], and []. The left side unpacks that object into three respective variables. I went into detail about unpacking with regard to the zip() method in these two posts.
Last time, I may have described things as if something magical was going on. This is not magic. The parentheses on the right represent a generator object and the commas on the left are to unpack it.
With regard to my previous discussion about making sure to use commas rather than equals signs, the latter…
a = b = c = (1, 2, 3)
…is basically doing:
c = (1, 2, 3)
b = c
a = b
b and c both get assigned (1, 2, 3).
With regard to the generator expression from above, this…
a = b = c = ( [] for i in range(3) )
…is basically doing:
c = ( [] for i in range(3) )
b = c
a = b
Because c cannot be assigned three different lists from the generator, Python assigns the generator to the variable a (and b):
<generator object <genexpr> at 0x0000019F45352200>
Lists, tuples, dictionaries, and strings are all iterable objects that have an iter() method used to return values. For example:
A for loop creates an iterator object, executes the next() method for each loop, and automatically stops at the end leaving us worry-free with regard to any potential [StopIteration] error:
Categories: Python | Comments (0) | PermalinkBacktester Development (Part 5)
Posted by Mark on November 21, 2022 at 06:54 | Last modified: June 22, 2022 08:36I am extremely happy to say that since this post, I have revamped the Python code into what seems to be a properly working backtester! Today I will begin to review the logic.
While not easy, the process of dropping an existing framework into a slightly altered skeleton was not as difficult as once feared. When I first ran the revised program on a Wednesday afternoon, I wondered how many hours the debug process would take. I imagined it taking days! Previous experience had shown program fixes to not fix and when they do, to be followed by other things in need of fixing. Total debug time ended up being six hours. I had bugs, but I was able to seek and destroy.
One of my biggest struggles was organizing the variables. This literally made my head spin and kept me paralyzed for a good 60 minutes. I still don’t have a good solution except to say time working with the program breeds familiarity. I would still like to include all variables in a function and just call the function to reset and manage. That may not be practical. What I now have for variables is much different than the original key shown here. At some point, I should probably update that index.
For variable initiation and reset, I was able to condense code length by zeroing out multiple variables in a single line:
> var_a = var_b = var_c = var_d = 0
This is legit.
It took me a couple hours, however, to realize this is not:
> list_a = list_b = list_c = list_d = []
While the labels are different, each actually points to the same exact list. Changing any of them will result in a change to all, which was a real mess. A proper way to initiate multiple variables as separate lists is:
> list_a, list_b, list_c, list_d = ( [] for i in range(4) )
Precision is very important, too. The following does not yield the same result:
> list_a = list_b = list_c = list_d = ( [] for i in range(4) )
Instead of all pointing to an empty list, which is not even I wanted, done this way they all point to a generator object.
The first two paragraphs of Part 4 explain why I needed to revamp the program with regard to inconsistent DTE selection, etc. I will now proceed to describe the restructuring in words.
The basic backtesting approach remains as described in the third paragraph here. It will become evident why I was able to reduce the number of control branches to ‘find_spread,’ ‘update_long,’ and ‘update_short.’ The wait_until_next_day loop remains at the top albeit with more limited application in the current version (14).
I will continue next time.
Categories: Python | Comments (0) | PermalinkMED Stock Study (2-10-23)
Posted by Mark on November 18, 2022 at 06:38 | Last modified: March 7, 2023 14:27I recently* did a stock study on Medifast Inc. (MED) with a closing price of $109.45.
M* writes:
> Medifast Inc is a US-based company that produces, distributes
> and sells products concerning weight loss, weight management,
> and healthy living. The company generates its revenue from
> point of sale transactions executed over an e-commerce platform
> for weight loss, weight management, and other consumable
> health and nutritional products.
This [as of 2021] medium-sized company has grown sales and earnings at annualized rates of 17.3% and 30.6% over the last 10 years, respectively. Lines are mostly up and parallel except for a 2-3-year dip between ’14 and ’17. Since ’16, lines look more up, straight, and parallel. PTPM has increased from 6.9% in ’12 to 14.2% in ’21 with a last-5-year average of 13.8%. This just beats the industry average.
ROE has increased over the last 10 years from 17.6% to an eye-popping 82.1% and far outpaces industry averages. Zero long-term debt makes this even more impressive. Debt-to-Capital (as leases) over the last five years averages 6.4%, which is far below industry averages. Current Ratio is 1.49.
I assume long-term annualized sales growth of 5% based on the following:
- YF projects YOY 4.2% and 0.6% for ’22 and ’23, respectively, based on one analyst.
- Zacks projects 2.8% YOY growth and 0.7% YOY contraction for ’22 and ’23, respectively (one analyst).
- Value Line projects 5.6% annualized growth from ’21-’26.
>
I assume long-term annualized EPS growth of 4% based on the following:
- MarketWatch projects annualized contraction of 1.4% and 0.9% for ’21-’23 and ’21-’24, respectively, based on one analyst.
- Nasdaq.com projects 2% YOY growth and 3.3% growth per year for ’23 and ’22-’24, respectively [2, 2, and 1 analyst(s) for ’22, ’23, and ’24].
- YF projects 9.7% YOY contraction and 5.7% growth per year for ’22 and ’23, respectively (one analyst), along with 5-year annualized growth of 20%.
- Zacks projects 6.4% YOY contraction and 2% YOY growth for ’22 and ’23, respectively (two analysts).
- Value Line projects annualized growth of 4.6% from ’21-’26 and 12.3% from ’20-’26, but the latter may be [artificially high and] less meaningful as a result of base effects (i.e. a 60% YOY jump in ’21 EPS).
>
This is about as scant as analyst coverage can be, which makes long-term forecast difficult. I basically have 20% and 4.6%. Given one estimate, I might halve to get an acceptable margin of safety. With two, I am just using the lower number.
My Forecast High P/E is 17. Over the last decade, high P/E has ranged from 17 (’13) to 56.5 (upside outlier in ’18) with a last-5-year average (excluding the outlier) of 26.4. I am taking the low end of the range.
My Forecast Low P/E is 6. Over the last decade, low P/E has ranged from 5.6 (downside outlier in ’20) to 18.2 (’16) with a last-5-year average of 12.1. I am targeting the lower end of the range.
My Low Stock Price Forecast is the default value of $79.10. This is 27.7% below the previous closing price. It is also 17.6% below the 52-week low and 57% below the 2021 low.
All this computes to an U/D ratio of 5.4, which makes MED a BUY. The Total Annualized Return is 22.4%.
MED has a hefty dividend yield close to 6%. After dividend inception in ’15, Payout Ratio has ranged from 40.9% (’21) to 71.8% (’16) with a last-5-year average of 51.2%. I am forecasting below the entire range at 40%.
PAR is 14.5%, which is outstanding for a medium-sized company.
I use Member Sentiment (MS) to assess margin of safety (MOS) by getting some idea how likely the company may be to outperform my estimates. Out of 176 studies over the past 90 days (my own and 104 others with projected low prices above last closing price excluded), projected sales, projected EPS, Forecast High P/E, Forecast Low P/E, and Payout Ratio average 10%, 8.7%, 20.5, 11.2, and 50.1%, respectively. I am lower on all inputs. Value Line projects an average annual P/E of 20, which is higher than MS (15.8) and me (11.5). MOS seems healthy here.
MS Low Stock Price Forecast is $77.15—about 3% below mine. This would lower my U/D just a tad while effectively lowering the Forecast Low P/E, which I already selected to be sufficiently low.
As an aside, I’m surprised to see so many [now] invalid studies due to Low Stock Price Forecast > previous close. These studies are all from the past 90 days when MED traded no higher than $130.31. Forecast Low Stock Price is $130 or higher in 77 of those studies, which means they were invalid from the outset.
>
*—Publishing in arrears as I’ve been doing one daily stock study while posting only two blogs per week.
Backtester Development (Part 4)
Posted by Mark on November 15, 2022 at 06:54 | Last modified: June 22, 2022 08:36Despite my best efforts described near the end of this post and the beginning of that, the DTE combinations I was getting from the short and long legs were not what I wanted. Another mitigating factor is that depending on the cycle, options may not be available in the 120-150 DTE range. Today I will discuss what I have done to revamp the logic.
One thing I found is despite check_second, sometimes I actually needed to check a third. This made me realize I may not know exactly how many I need to check, which suggests a need for dynamically-created variables.
I’ve run up against this issue a number of times so it’s worth trying to describe a more general case and look for a solution.
Suppose I have a large data file and I don’t know how often a pattern will repeat. Each time it occurs, though, I need to capture details and later evaluate to select which one I’ll need.
What is the best way to name variables in this case?
One approach would be to define as many or more sets than I’ll ever need. For example:
> dte0 = dte1 = dte2 = dte3 = … = dte50 = 0
> row_num1 = row_num2 = row_num3 = … = row_num50 = 0
That seems like a pain.
I’d rather define and initialize the variables with a loop like:
> variable_set = [‘dte’+ str(i) for i in range(50)]
> print(variable_set)
Now I’ve got the variable names in a list… could I then initialize them as a loop?
This doesn’t work:
> for i in len(variable_set):
> int(variable_set[i]) = 0
>
> print(variable_set)
Even if that did work, I might have just changed the list to all zeros, which destroys variable names I might otherwise be able to use later in the program. I don’t know how to keep the strings as variable names for later use.
I think the solution is to use a list or dictionary and add values as needed. I can’t address particular values by unique names, but the logic in the program can dictate what values to retrieve by index/slicing and where to insert/delete the same way.
Here’s what I know:
- Short option should have at least 60 DTE.
- Long option should be next month out (for now).
- If I can identify the short option first then the long option will come easy.
- Iterating down the file means going from longer-term to short-term options, which makes this more complex.
>
The new solution is to encode any necessary data for options 200 DTE or lower at the proper strike price and put them in lists (e.g. dte_list, orig_price_list, orig_delta list, etc.).
Each time DTE changes, as long as historical date remains the same I will encode new data for the proper strike price. Instead of appending values to the end of lists, I will insert at the beginning.
The time to identify the spread is once historical date changes. dte_list then includes DTE of all available options on that historical date in ascending order. I can then iterate through the dte_list from beginning to end. If the value > 60 then I have found my short option and the long option will be next (for width = 1). I can then assign the appropriate values from each list to specific variable names like those listed in the key.
Categories: Python | Comments (0) | Permalink

