Option FanaticOptions, stock, futures, and system trading, backtesting, money management, and much more!

FOXF Stock Study (1-31-23)

I recently* did a stock study on Fox Factory Holding Corp. (FOXF) with a closing price of $113.39.

Value Line writes:

     > Fox Factory Holding Corp. designs, engineers, manufactures, and
     > markets performance ride dynamics products for customers
     > worldwide. Fox Factory Holding is the holding company of Fox
     > Factory, Inc. The company’s premium brand ride dynamics products
     > are used primarily on bicycles, side-by-side vehicles, onroad
     > vehicles with off-road capabilities, off-road vehicles and
     > trucks, all-terrain vehicles, snowmobiles, specialty vehicles
     > and applications, and motorcycles.

This medium-sized company has grown sales and EPS at annualized rates of 19.8% and 25.5% over the last 10 years. Lines are mostly up and parallel with slight EPS pullbacks in ’15 and ’20. PTPM over the last 10 years has increased from 9.5% to 14.5% with a last-5-year average of 13.8%. This beats peer (stated as THRM, ALSN, and GTX) and industry averages.

ROE has trended down from 28.5% in ’13 to 19.2% in ’21 with the last five years averaging 20.3%: slightly better than peer and industry averages. Debt-to-Capital has ranged from 8% (’13) to 35.5% (’20) over the last nine years with a last-5-year average of 25.2%: lower than peer and industry averages. Interest Coverage is 28, and the company has no long-term debt.

I assume long-term annualized sales growth of 8% based on the following:

I assume long-term annualized EPS growth of 9% based on the following:

I’m forecasting beneath the range (12.6%) of four long-term estimates.

My Forecast High P/E is 27. High P/E has ranged from 26.1 (’14) to 51.1 (’20) and trended higher since 2013. The last-5-year average is 42.9.

My Forecast Low P/E is 15. Low P/E has ranged from 14.7 (’16) to 26.3 (’21) since 2013. The last-5-year average is 20.6.

My Low Stock Price Forecast is $67.20, which is the default value. This is 40% less than the previous closing price and just below the 52-week low of 69.3. The stock has had a big run-up in price over the last few months.

All this results in an U/D ratio of 1.6, which makes FOXF a Hold. Total Annualized Return (TAR) is 10.4%.

While TAR is decent, PAR (using Forecast Average, not High, P/E) is only 5%. I want more from a medium-sized company.

For more context, I like to assess margin of safety (MOS) by comparing with Member Sentiment (MS). Out of 212 studies over the past 90 days, projected sales, projected EPS, Forecast High P/E, and Forecast Low P/E average 12.9%, 13.2%, 31.3, and 18.9, respectively. I’m lower on all inputs and also lower than Value Line’s projected average annual P/E of 25 (vs. 26.1 for MS and 21 for me). The average MS Low Stock Price Forecast is also above mine at $68.87.

The MOS is alive and well in this study: enough to preclude a buy over $97/share.

*—Publishing in arrears as I’ve been doing one stock study per day while usually posting two blogs per week.

Backtester Logic (Part 9)

Having completed analysis of the first two, I now continue with the update_long and update_short branches.

The conditional skeleton for these two branches of program flow looks like this:*

As discussed in the third paragraph of Part 2, rather than follow trade entry criteria these branches just need to locate previous selections. L134 (for update_long) and L150 (for update_short) look to match strike price and expiration. Short and sweet.

Here are considerations with regard to historical date:

  1. After find_short, historical date should be advanced to update the spread.
  2. After update_long, historical date should not be advanced since two legs need to be updated on the same date.
  3. After update_short, historical date should be advanced assuming the trade remains open.

(1) Near the end of find_short, the wait_until_next_day flag is set to True, but current_date has not yet been assigned. I have already discussed this oversight (near end of Part 3 and beginning of Part 4).

(2) update_long does nothing with the wait_until_next_day flag, and…

(3) In L145, update_short checks to make sure historical date has not advanced before progressing to update. If historical date has advanced, then an exception is raised. This should not happen if the data file is complete unless the short option has expired. Whether it be short option expiration or a predetermined DTE, I still need to build in time-stop logic to handle this.

When any branch of program flow finds a match, encoding from the data file assigns to variables shown in the key.

find_short and update_short include several lines devoted to variable reset. Variable reset prevents previous (stale) values to be used in current calculations. Variable reset will only be done as part of find_short if a short option is not found, which itself in an error. To do this effectively, I will make a list to ensure every variable involved in assignment is subsequently reset.

I will also decide whether a user-defined function to reset variables (as mentioned in Part 2 footnote) is indicated.

Is that the light at the end of the tunnel I might be starting to see?

Here are some other topics I still want to discuss:

I will continue next time.

*—As discussed in the Part 6 footnote, I’ve used Atom’s folding feature for display purposes, which
     means the higher level (indented) lines are hidden from view.

BOOT Stock Study (1-26-23)

I recently* did a stock study on Boot Barn Holdings Inc. (BOOT) with a closing price of $74.37.

From M*:

     > Boot Barn Holdings Inc operates specialty retail stores.
     > The company sells western and work-related footwear,
     > apparel, and accessories in the United States. It is a
     > single operating segment, which includes net sales
     > generated from its retail stores and e-commerce websites.

This medium-sized company has grown sales at an annualized rate of 18.5% over the last 10 years and EPS 41.8% per year since ’14. Lines are mostly up, straight, and parallel. PTPM over the last 10 years has risen from 0.6% to 17% (upside outlier) with a last 5-year average (excluding the outlier) of 6.6%. This was below peer (stated as BURL and VSCO) and industry averages until ’20.

ROE has increased from 10.1% in ’14 to 34% in ’21 with the latter appearing to be an upside outlier. The last 5-year average is 18.9%, which seems slightly below (above) industry (peer) averages. Debt-to-Capital has averaged 45.7% over the last five years and was higher than peer and industry averages until 2019. The company has no long-term debt [operating leases], though, and Interest Coverage is over 60.

I assume long-term annualized sales growth of 6% based on the following:

I assume long-term annualized EPS growth of 5% based on the following:

My Forecast High P/E is 20. High P/E has ranged from 19.3 (’17) to 93.1 (upside outlier in ’15) since 2014. The last 5-year average is 25.6.

My Forecast Low P/E is 6. Low P/E has ranged from 4.9 (’19) to 31.3 (upside outlier in ’14) since 2014. The last 5-year average is 7.2.

My Low Stock Price Forecast is $36.50. This is the default value and 51% below the previous closing price. While this is more than the 20% rule of thumb, given the earnings contraction projected over the next couple years, I don’t see good reason to override (which would effectively be using a higher Forecast Low P/E).

All this results in an U/D ratio of 2.1, which makes BOOT a Hold. Total Annualized Return is 15.9%.

While the total return projection is impressive, PAR (using forecast average, not High, P/E) is a lukewarm 6.3%. I want more from a medium-sized company.

For added context, I like to assess margin of safety (MOS) by looking to Member Sentiment. Out of 120 studies over the past 90 days, projected sales, projected EPS, High P/E, and Low P/E average 10.7%, 9.8%, 21.9, and 8.5, respectively (two studies excluded with Projected High P/E’s of 100 and 2128, which skewed the average Projected High P/E to 39.8). I’m lower on all inputs. Same goes for the average projected low price of $41.90, which is $5.40 higher than mine.

Finally, Value Line has projected average annual P/E at 17 compared to my 13.

It will suffice to say that the MOS is alive and well in this analysis. I’m a buyer on a stock price below $66.

*—Publishing in arrears as I’ve been doing one stock study per day while usually posting two blogs per week.

Backtester Logic (Part 8)

Having tied up several loose ends in the last post, today I want to analyze logic for the find_short branch.

As with find_long (see here), find_short involves a multiple if statement:

The input variable (see key) from the top of the program is defined as follows:

      width = int(input(“Enter spread width (in monthly expirations): “))

L98 searches for a DTE between upper and lower bounds. The difference between bounds is 10 days. The upper bound is the long option expiration minus (28 * width). With 28 or 35 days between expiration cycles, (28 * width) is the highest value it should ever take. The extra 10 [days] should accommodate a 35-day cycle.

If the width covers two (or more) 35-day cycles then this may not work because 7 * 2 = 14 days is more than the 10 provided. Moving the 10 inside parentheses won’t work because width = 4 (the least required to get two 35-day cycles) would allow for a lower bound that is ((4 * 28) + 40) below the upper bound: enough to capture two potential options for the short leg (although the desired longer-dated option will match first if the data file is complete).

To better code L98, I should include the additional days for each three months of width:

      DTE > L_dte_orig – ((28 * width) + (10 * ((width + 2) // 3)))

Floor division ( // ) truncates the remainder for positive numbers. For one month, this is 3 // 3 = 1. For three months, this is
5 // 3 = 1. Only when I get to four months, which is 6 // 3 = 2, will 20 additional days be included.

I will make this modification, but for a couple reasons it’s not something I will use anytime soon. First, I really worry about liquidity and option availability for width > 3 because the long will then be very far from expiration and probably lightly traded. Practically speaking, I would only consider a max width of two months. Second, the lower bound seems unnecessary. Any option available in the long expiration should be available in the short as long as the data file is complete.*

If I want to clean the data files, which includes assessing completeness by searching for omitted and erroneous data, then I can create some simple Python scripts. That’s a topic for another post.

L99 looks to match strike price with the long option. One option per expiration cycle should match. I am okay with this line coming after L98 because the more restrictive if statement (see Part 4) may depend on how many expirations are available: a variable number over the years.

L99 also looks to match date with long option purchase. This is redundant because the previous if statement (not shown) checks for this and makes an entry in missing_s_s_p_dict if short not found.

I will continue next time.

*—This brings to mind another problem with 10-point multiples in find_long (see fourth paragraph Part 7).
     Far from expiration, I have sometimes noticed that only 25-point strikes are available (and only 50-point
     strikes going back many years). Requiring 10-multiple is actually constrained to 50-point strikes in that
     case. The difference between one 50-point strike and the next can be much more than 1% of the
     underlying in those earlier years, which is enough to make for a clearly directional trade.

Introduction to BetterInvesting

I mentioned BetterInvesting in the last paragraph here. This is something I plan to use to help manage individual stock positions as part of a longer-term portfolio.

I subscribed to BetterInvesting late last year after finding the organization in fall 2021. From their website:

> Today NAIC [National Association of Investment Clubs] is known as BetterInvesting, a 501(c) (3)
> nonprofit association that remains dedicated to helping individuals and investment clubs learn and
> practice our fundamental approach to stock investing. Passionate volunteers, who follow our
> principles and practice the SSG [Stock Selection Guide], teach our educational programs. The SSG
> is available online 24/7, making it even easier to study and invest in stocks.

I don’t think of it like a service to be sold as much as I do a tool for analyzing stocks. Their approach covers a wide breadth and if someone is going to do fundamental analysis to any degree, then something to organize all the inputs and a process guiding what to do with them can be extremely valuable. One could do without such a tool and if this is something you try, then I’d be interested to hear what you do to get all the information and just how long it takes.

Personally, learning the BetterInvesting approach has given me a process to gather relevant data and make investment decisions in 1-2 hours per stock. Prior to this, I really had no idea. If I wanted to make decisions based on fundamental analysis, then I could look at suggested buy lists, perhaps investment newsletters (which I think are generally a waste of money), and some other places.

In 2001, I developed a stock screen after some book reading that was effectively the beginning to managing my own portfolio. It took me about an hour per week and I fared well with it on an absolute basis. I never revisited this once I discovered options in 2006. While I’ve thought about rekindling the original stock screening effort in recent years, I have also become a true believer in classic fundamental analysis for longer-term purposes. This is more the BetterInvesting way.

Any explanatory content I publish under the category BetterInvesting might be better studied in the video education library on their website since I’m a new member rather than an official instructor or volunteer. I might actually like to become a volunteer if it got me out to the community to teach the process to other like-minded individuals.

This would then beg the question whether their process is the best? I think we’re far away from having enough information to answer that question with any validity. I believe having a process and sticking with something repeatable to optimize efficiency is probably as good as anything else.

Backtester Logic (Part 7)

Today, I want to tie up some loose ends related to multiple topics.

The issue of trading options farther out in time (see paragraphs 3-4 of Part 6) will resurface later when I discuss time spread width. For now it will suffice to say that if I want to increase width, besides trading a longer-dated long option I can also do a shorter-dated short option when slippage (open interest, volume, liquidity, etc.) is a concern.

L71 (see Part 4) filters for a 10-point strike by requiring the remainder of strike price / 10 = 0. Due to liquidity concerns, I would prefer to trade only 25-point strikes. My second choice would be 10-, and 5-point strikes would be last. I don’t have actual execution data to support this—it’s just gut feeling based on what I’ve anecdotally heard from other traders.

One potential issue with filtering for strike multiple is a strike density decrease with lower price of the underlying. The relative difference between one 10-point strike and another generally gets larger going back in time where SPX is valued lower. With SPX at 4000, 10 points is only 0.25%. With SPX at 1500, though, 10 points is 0.67%. I cannot say how significant a concern this is, but it would be 2.5x worse for 25-point strikes. The base strategy is not intended to be directional.

Going back to L70, I no longer need the lower bound with my proposed solution in the second paragraph of Part 6. The program will find the first option that has less than (((mte + 1) * 30 ) + 5) DTE. The extra five along with 30 (two more than the more common 28-day) will cover a 35-day expiration cycle, which happens once per quarter. The option I want will be that or the next-lower DTE option to match. The latter will be checked by the check_second flag.

Rather than nested ifs, I could code as an if-else block per fifth paragraph of Part 4, but I don’t see any real advantage. Additional logic in the else block may dictate that nothing be executed for the current iteration (only 0.4% of rows are used). I would have to include a pass (null) statement or two (as if-elif-else) since if-else forces execution of one branch or the other.

In the second paragraph of Part 4, I said the update_long logic avoids a subsequent match to options on the same date even though the same-date options are not being skipped. That logic is in L134:

This looks to match strike price and expiration date. Recall the third paragraph here. Once find_short is complete, same-date options are avoided despite the wait_until_next_day flag being circumvented because each strike/expiration combination only appears once per historical date. Is this more or less efficient?

Although I may be wrong due to “mitigating factors” (second-to-last paragraph of Part 4), I would hypothesize matching strike price and expiration date to be less efficient than skipping dates with wait_until_next_day. The latter requires one truth test to compare current_date and historical date whereas update_long’s and statement requires two.

Loose ends be gone!

Back from the Hack

My apologies for any “404 error” you may have gotten recently when trying to navigate my website. The site was hacked by malware. I don’t know if it was down for days, for weeks, or for months. I subscribed to a security package and have been told that everything is now restored.

2022 has been a tough time for trading: my most difficult since beginning full-time in [and including] 2008. This year has left me staggered and raw. It’s attacked my positivity and hope. I will blog more about this in coming posts.

Despite the hit, I’m not ready to give up yet. Throwing out the baby with the bathwater is never a good idea and to that end, I still have work to do.

The Python backtester is on life support. You will continue to see posts on Python work I did earlier this year. For me, the dagger was realization that the data and corresponding results were irrevocably compromised.

While I haven’t practiced Python for several months, the next time I revisit will be my fourth “tour of duty.” I got farther the second time in 2020 than I did the first (2019). The third (2021-2022) was mostly practical application in working with the backtester. Hopefully the fourth comes easier and allows me to reach greater depths. Backtester aside, I have other projects in mind for which programming may be useful. Stay tuned for additional blog posts in the Python category.

Thankfully, some new automated backtesters have come onto the market. I have taken a good look at Option Omega, which has many appealing features. I also recently learned about MesoSim, which may be more tailored to my kind of trading.

I’m not really sure whether subscribing to one of these services solves the data issue. It’s at least likely to hide data flaws because close scrutiny of the backtrade log will be required. Making matters worse, some automated backtesters don’t provide such a log with essential data like entry and exit prices. This would allow for verification especially when trade results are highly discrepant from anything seen in ONE where the complete option chain is on display.

Either way, I have not entirely given up on backtesting and still have option strategies I’d like to study with an automated product. I will blog about this in the near future.

Something I have discovered in the past several months is BetterInvesting, which focuses on technamental [see Take Stock by Ellis Traub (2010)] analysis of stocks. Most important to me in this pursuit would be a repeatable process, which BetterInvesting provides. I don’t care so much what the process is because testing/validating the process would require [backtesting] tools that I’m not even sure exist. Regardless, you can expect some future blog posts in this area under the new category tag “BetterInvesting.”

Backtester Logic (Part 6)

Today I want to finish ironing out the logic from L70 shown here then continue to analyze that conditional skeleton.

As an example of my proposed solution at the end of Part 5, in 2015 (date 16448) we have options with 94 DTE followed by options with 66 DTE. Both pass L70, which means the latter is what I want. I can add a Boolean flag check_second that starts out as False. Once int(float(stats[2])) no longer equals L_dte_orig (see key), the flag gets changed to True. If this passes L70 then find_long continues in this DTE. If it fails L70, then change control_flag to find_short and continue (to the next iteration) without setting wait_until_next_day to True.

In addition to the 60 – 95 DTE range, I am also interested in studying longer-dated time spreads despite potential issues with slippage. Examples include 90 – 125, 120 – 155, and 150 – 185 DTE. Live trading is the best way to understand slippage. Unfortunately, I can’t go back and live trade in previous years. Common wisdom suggests slippage will be lower with higher volume and open interest, but the data files don’t allow me to test this. I could plot volume and open interest over time, but this may be a waste of time since I have no way to know how that might translate to slippage.

Another thing that may limit my ability to backtest longer-dated options is historical availability. Over the years, more expirations and more strike prices have come available coincident [I suspect] with higher-volume option trading. In 2015, 168 DTE is available followed by 105 DTE, which means I can’t do a time spread one month wide. Things change in 2020. In early Jan 2015, monthly expirations appear for the first four months. In early Jan 2020, monthly expirations appear for the first six months. This expresses my concern despite being anecdotal observation.

One thing I don’t see in the conditional skeleton is a continue statement at the bottom. Whether or not the backtester identifies the current row as the long option, it can then advance to the next iteration (row). Without a continue statement, the program will go on to unnecessarily evaluate control_flag three more times before advancing. “Continue” should conclude the first three branches. Completing the block, the last branch doesn’t need a continue statement as advancing to the next iteration will automatically take place.*

I will press forward next time.

*—Actually, I did remember this. The Atom text editor features folding to hide blocks of code as described
     here. With “continue” being the last line of the previously indented blocks, they were hidden.

Time Spread Backtesting 2022 Q1 (Part 9)

Today I will conclude my time spread backtesting for 2022 Q1.

I left off at the second adjustment point of trade #14. Pre-adjustment, this is very close to max loss.

Whether the mere act of adjusting makes ROI% better or worse is simply a matter of arithmetic. The first adjustment worsens ROI% from -7.4% to -8.1%. The second adjustment improves ROI%. MR increases by $1,388, which is a bigger change than that resulting from the first adjustment (+$568). PnL pre-second-adjustment is -19% vs. -7.4% for the first, though. To maintain ROI%, the larger MR would have to be accompanied by a loss that is $263 higher. Transaction costs are fixed at $21/contract, which is $84/adjustment. Because $84 < $263, the second adjustment dilutes ROI% back to -16.8%:

At this point, TD = 11. According to ONE, I only have ~1% of downside before -20% max loss is reached.* Is the adjustment worth doing? Certainly in the absence of a guideline preventing it (see base strategy in Part 1), I have to say yes.

Max loss is hit at -21.4% four days later on a 2.12 SD market decline. For the trade, SPX falls 1.88 SD over 29 days although it’s only down 1.34 SD over 28. Unpredictability of large moves is one reason this endeavor can be so difficult.

In total for 13 trades entered in Q1,** the base time spread strategy makes $1,855 on a max margin of $10,252. The latter also represents the greatest increase from initial capital (+45%) seen in 158 historical trades. Although it’s probably far more than I need, I like to err on the side of conservatism. Doubling the $10,252 will provide for a substantial margin of safety.

The time spread return is therefore +9.0% for Q1 2022 compared to -12.8% for SPX. That is a shellacking with which I would be quite satisfied (compare here). The overall PF is low (but profitable!) at 1.28. The average win (loss) is $938 (-$1,647) on 9 (4) total wins (losses). Over just 13 trades, the max consecutive number of wins vs. losses is two vs. eight, respectively: pretty healthy [for equity curve to hang out near all-time highs most of the time]. Those two started out the year, though, which made for what would ultimately be the max drawdown (MDD) of -15.7%.

Rounding up to four total months of backtesting, the annualized time spread return is +27%. As always, don’t count on any good result repeating every year.

Taking a normalization approach, MR for each trade can be divided into any fixed number you choose to get position size. The PnL numbers then turn out slightly different. Normalizing for $100K, I get a Net PnL of $21,541 (+21.5%) and a PF of 1.21, which is slightly worse than without normalizing (heed the less-impressive result). MDD after the first two trades is about -49%. That’s way too high for my risk tolerance. MDD would be -19.6% if normalized for $250K MR. I find this more reasonable. At this level, total return falls to +8.6%, which is +25.8% annualized.

* — The automated backtester would not have access to this information.
** — 14 includes trade #3 starting 3/9/22 and trade #11 starting 3/7/22. I took the worst
        of the two since both occurred in the same week.