Unlocking Sequential™ in EasyLanguage via Dueling Finite State Machines

Disclaimer:
“Sequential™” is a registered trademark of Tom Demark. This post presents an independent, educational interpretation of the components of the Sequential™ pattern as described in Tom Demark’s book, The New Science of Technical Analysis. The analysis, opinions, and code examples provided herein are solely those of the author and are intended for informational and educational purposes only. This work is not affiliated with, endorsed by, or officially connected to Tom Demark or any related entities.

Sequential™ Pattern – Setup and Countdown

This pattern is fully described in Tom Demark’s book and consists of two distinct phases.  For brevity’s sake, I will just discuss the buy setup.  This indicator is designed to help determine when a trend is becoming or has become exhausted.  Unlike a trend following indicator that helps you get in at the genesis of the trend, Sequential indicates when to take an opposing position after trend termination.  This post doesn’t concern itself with the efficacy of Sequential, but with the process of programming such a difficult pattern and all the conditions that it involves.  The indicator consists of two parts or phases. The Setup phase is stringent, requiring that the same price pattern occur for at least nine consecutive days (bars). In contrast, the Countdown phase is less strict; it mandates that a different price pattern occurs over a span of 13 days (or bars), although these occurrences do not need to be consecutive.  Setup is complete when an “intersection” occurs, marking the point where prices start to brake or consolidate. Countdown, on the other hand, is finished when the 13th instance of the designated price pattern is observed. Because the pattern in the Countdown phase does not have to appear on consecutive bars, this phase can take many days to complete.  During Countdown, three scenarios can occur that either restarts or recycles the process.

Setup – Sequence

  1. close[0] > close[4]
  2. followed by nine consecutive Close[0] < Close[4]

Intersection- Sequence

  • If nine bars fulfill the Setup then examine the following
    • Bar 8  High[1] > Lowest(Low[4],5)
    • Bar 9  High[0] > Lowest(Low[3],5)

Countdown – Sequence

  • 13 days or bars fulfill Close[0] < Low[2] over any number of days

Sequential™ – Completion, Restart, Recycle

  • Completion – once Countdown reaches 13, the Sequential pattern is completed.
  • Restart – during countdown if a Close > Highest High during setup or a Sell Setup occurs – start either from scratch or start the Sell Countdown
  • Recycle – a new Buy Setup occurs, then start from the Countdown phase again.

Because we have to monitor a Sell Setup during the Countdown phase, we need to run two Finite State Machines concurrently.  These two state machines will duel with each other.  Both searching for their own solutions and knocking each other out during the process.

Finite State Machine Structure

Years ago, I embarked on building a theoretical compiler—a challenging project I nearly finished. The initial step was to create a parser that transforms high-level code into tokens according to the language’s grammar. In doing so, I learned about Finite State Machines (FSMs) as my program processed source code one character at a time and used FSM logic to build a token table.

Sample FSM to Find Pivot Highs and Pivot Lows

From this experience, I quickly discovered that even the most complex patterns can be detected using a Finite State Machine. Below is a graphical representation of a simple FSM that identifies the following pattern that can take up to 90 days (bars) to complete.

  1. A high pivot with strength 2

  2. Followed by a low pivot with strength 2

  3. Followed by another high pivot with strength 2

In this example, a pivot is defined such that the central (or “pivot”) bar must have a higher high (for a high pivot) or a lower low (for a low pivot) than both the two bars preceding it and the two bars following it. Additionally, I also added the high pivot requires that the two prior bars exhibit ascending highs and that the two subsequent bars exhibit descending highs, while a low pivot follows the opposite pattern.

Finite State Machines (FSMs) consist of a limited number of states that describe the various conditions of a system:

  • Start State: The initial point where processing begins.  Looking for the first Pivot High.
  • Intermediate States: The stages the FSM progresses through as it processes input.  Looking for the first Pivot Low.
  • Terminal (or Accepting) States: The final state(s) indicating the system has completed its task.  Locating the final Pivot High.

Transition logic (the “rules” for shifting between states) guides the FSM’s movement, and some FSMs include a timeout function that resets the machine if it stays in one state too long.

Transition from FSM State 0 to FSM State 1

Acting like Pac-Man, the FSM gobbles one bar at a time and looks for this pattern:

  • high[2] >high[1] – right side
  • high[1] >high[0] – right side
  • high[2] > high[3] – left side
  • high[3] > high[4] – left side

Transition from FSM State 1 to FSM State 2

Now we look for the specific low pivot pattern

  • low[2] <low[1] – right side
  • low[1] <low[0] – right side
  • low[2] < low[3] – left side
  • low[3] < low[4] – left side

Transition from FSM State 2 to Completion and then back to FSM State 0

The pattern is completed after the subsequent high pivot pattern is confirmed.  Once completed the machine resets itself to FSM State 0.

  • high[2] >high[1] – right side
  • high[1] >high[0] – right side
  • high[2] > high[3] – left side
  • high[3] > high[4] – left side

FSM Clock Override

If the pattern is not recognized within 90 days or bars from the first pivot high, then the machine resets back to FSM State 0.

Pivot Point FSM Output

Simple FSM Output

Switch Case in EasyLanguage

When I started programming in Python, the Case statement was not included which shocked me.  Since Python 3.10 it has been introduced.  A Switch Case structure lets a program choose among several execution paths based on a variable’s value, using distinct cases instead of long if-else chains. This results in cleaner, more efficient, and more readable code.  Take a look at the syntax of the Switch Case in EasyLanguage – remember much of the following code is dedicated to painting the bars.


Inputs:
PivotStrength(3); // Strength parameter for pivot high detection

Variables:
FSMState(0),barCount(0),j(0); // FSM state: 0 (none), 1 (first), 2 (second), 3 (third)

switch (FSMState)
begin
case 0:
begin
if h[2] > h[3] and h[3] > h[4] and h[2] > h[1] and h[1] > h then
begin
FSMState = 1;
Print(d," First pivot high found: ",h[2]);
barCount = 0;
for j = 0 to 4
begin
plotPB[j](h[j],l[j],"FSM PVT Patt.",yellow);
end;
end;
end;

case 1:
begin
if l[2] < l[3] and l[3] < l[4] and l[2] < l[1] and l[1] < l then
begin
FSMState = 2;
for j = 0 to 4
begin
plotPB[j](h[j],l[j],"FSM PVT Patt.",red);
end;

end;
end;

case 2:
begin
if h[2] > h[3] and h[3] > h[4] and h[2] > h[1] and h[1] > h then
begin
FSMState = 0;
for j = 0 to 4
begin
plotPB[j](h[j],l[j],"FSM PVT Patt.",cyan);
end;
end;
end;

end; // End case-switch

barCount = barCount + 1;
if barCount = 90 then
FSMState = 0;
Simple FSM to locate Pivot Point Pattern

The syntax is straightforward: the keyword switch is used, and the variable FSMState directs the flow through different case blocks. Initially, FSMState is set to 0, and a transition occurs only when the specified criteria are met. Once met, FSMState is updated to 1. EasyLanguage follows a non-fall-through paradigm—once a state transition occurs, no other case statements are evaluated during that iteration; the program simply reaches the end of the block and awaits the next cycle. By contrast, in some languages, when the state changes (for example, from 0 to 1), the corresponding case for state 1 may be evaluated immediately within the same cycle.

Is it as Complicated as it Looks?

Not at all.  Look at the different case blocks and you will see a very similar structure.  As stated earlier the code to paint the bars take up 12 lines of code.  The timer is located at the bottom of the code – once barCount = 90, the FSM resets to 0.

Is Sequential ™ Easy to Program with a Finite State Machine?

I wouldn’t say easy, but I wouldn’t say hard either.    With a little elbow grease and knowledge of how TradeStation works, and some EasyLanguage knowledge it is not difficult.  If it were easy, you would see the code all over the place.  I have previously programmed parts of it in my Easing into EasyLanguage books.

You might think you could have ChatGPT generate the code for you, and for laughs, I tried asking ChatGPT to program Sequential™ using FSM and Switch/Case in EasyLanguage. While the output provided a foundation, most of the syntax was off, and the patterns weren’t defined properly. Ultimately, I discovered that simplifying the design—by using separate FSMs for the buy side and the sell side—made the implementation more manageable. To simplify the process, I focused exclusively on the buy side at first. I figured that once I had programmed the complete pattern for the buy side, adapting it to the sell side would be as simple as reversing the logic—since the overall structure remains identical.

Using “Backward Scanning” for the Setup Phase.

Setup requires analyzing at least 10 consecutive bars. When you hear “consecutive,” think of looping through each bar in sequence. A stringent consecutive pattern is best uncovered by looping back through historical data. For example, you can loop through the bars to identify a sequence of nine consecutive bars where the close of the current bar is less than the low from four bars prior. However, immediately preceding those nine bars, you must verify that the prior bar’s close is greater than the close from four days earlier. This additional condition ensures the pattern begins under the correct circumstances.

Is there a Method to this Madness?

I’ve found that using methods in EasyLanguage is a real asset during development. Methods work like functions but are local to the module in which they’re defined, so all the code is right there for easy reference, debugging, and testing. I typically reserve methods for code segments that I’ll reuse multiple times, which helps keep my project organized and efficient as it grows.  Here is the method that uses back scanning to uncover the Sequential Setup phase.

method bool seqBuySetup(int numConsDays)
var: int result;
begin

result = countif(c[0] < c[LookBack],suTarg);
if result = numConsDays and c[suTarg]>c[suTarg+LookBack] then
return(True)
else
return(False);
end;
Notice the syntax of the method structure

You might not recognize all parts of this code at first glance. First, I’m using a method, and I must specify its return type as bool (Boolean) in the method header. This indicates that the method will return either True or False. I’m also passing an integer variable, numConsDays, into the method.

Inside, I use the countIf function to evaluate the relationship between close[0] and close[4]—with LookBack set to 4. Essentially, countIf counts how many times the condition close[0] < close[4] is met over a span of bars defined by suTarg (or SetUpTarg). If this count equals 9, I then compare close[9] with close[13]. If close[9] is greater, I determine that this portion of the Setup phase has been successfully completed.

Does close[0] mean today’s close or the close of what George has penned as the close of the Focus Bar.

  • close[0] – close of today – not really unless it is the last bar on the chart – we could call this the Focus Bar if not
  • close[1] – yesterday – 1 day back or 1 day prior to the focus bar
  • close[2] – 2 days prior
  • close[3] – 3 days prior
  • close[4] – 4 days prior

When you run a backtest in TradeStation, the engine iterates through every bar in your dataset—even though your chart window only shows a subset of those bars. To make this clear, I use the term Focus Bar for whichever bar is currently being processed as the system moves from left to right. Think of it like a big loop over all bars: when the loop index is 50, bar #50 is the Focus Bar and you reference its values with [0] (e.g., close[0]). When the loop advances to index 51, bar #51 becomes the Focus Bar—still accessed with [0]—and so on.

Here’s the key point: all price series—high, low, open, etc.—are zero‑indexed, so the Focus Bar is always referenced with index 0. That means the Focus Bar’s closing price is close[0], its high is high[0], and its low is low[0]. For instance, if the Focus Bar is dated January 3, 1999, and it completes the Setup phase, you’d paint it using high[0] and low[0]. Remember, close[0] only represents “today’s” close when you’re on the very last bar; otherwise, it simply refers to whatever bar is currently the Focus Bar during a historical back-test.

Take a look at the following method to see this in action.

method void paintBuySetup(int numPaintDays)
var: int j;
begin
PlotPB[suTarg](High[suTarg],Low[suTarg],"DMSeq.",cyan,3);
for j = 0 to numPaintDays-1
begin
PlotPB[j](High[j],Low[j],"DMSeq.",yellow,3);
end;
end;
Looping from bar 0 to bar 8 or nine bars

Notice how I loop from 0 to numPaintBars-1 or 8 to paint the last nine bars in the sequence.  If you want to paint bars in a back-scan make sure you use the same syntax I have used here.  Use an offset for the PlotPB along with the same offset for each bar’s high and low in the loop.

 PlotPB[j](High[j],Low[j],"DMSeq.",yellow,3); // j goes from 0 to 8

If I want to compare the 10th bar in my series, then I refer to it as close[9].  I compare the 10th bar with 13th bar (close[9 + 4]) to see if I have the genesis of the Setup phase.  Bar number 0 is the first bar in the series going back in time (Focus Bar.)  Could I offset everything by one bar to get rid of the 0 offset?   Many people have a problem dealing with 0s so you could but if you want to turn this into a strategy and you want to execute on the next bar’s open, then you will need to stick with the 0.  Let’s break each state down and you will see how I was able to program this monster.

FSM State 0 – Scanning for the first part of the Setup Phase

    Case 0:
// Find a long setup - retroactively
if seqBuySetup(suTarg) then
begin
stateBuy = 1;
SetupCountBuy = suTarg;
SetUpHigh = highest(h,suTarg);
if plotBuySetUp then
paintBuySetUp(suTarg);
end;

If the seqBuySetup method returns True, the FSM transitions to State 1 and SetupCount is set to 9. This triggers a look-back over the past nine bars to determine the highest high, which serves as a reset level in the Countdown phase if a close exceeds that value.

In the PaintBar routine, the user can choose to display the Sequential Buy Setup, the Sell Setup, or both. If the user opts to display the Buy Setup, the paintBuySetUp method is executed.

FSM State 1 – Looking for an Intersection

This state uses a hybrid approach to detect an intersection. Now that we’re in FSM State 1, we’re close to completing the first phase. We start by examining bars 8 and 9, which are the final two bars of the nine-bar setup from the latest data. First, we check if the high of bar 8 is greater than any of the low values in bars 5, 4, 3, 2, or 1—if it is, an intersection is found. If not, we compare the high of bar 9 with the low values in bars 6, 5, 4, 3, or 2. Should bar 9 also fail to meet the criteria, we then switch to forward scanning for any subsequent bar that fits the criteria. Note that the bars identified in the forward scan do not have to meet the same stringent conditions as the consecutive bars in the Setup phase.  Eventually, a bar will meet the criteria, and we then can move to the Countdown phase also known as FSM State 2

    Case 1:
// Setup complete now look for intersection
// may take a couple of days
if SetupCountBuy = suTarg then
begin
//going back to get lowest lows of 5 bars
//prior to bar 8 and bar 9
minLow1 = lowest(low[4],5);
minLow2 = lowest(low[5],5);
if (High[1] > minLow1 or High[2] > minLow2) then
begin
stateBuy = 2;
CountdownCountBuy = 0;
if close <= Low[2] then
begin
CountdownCountBuy = 1;
if PlotBuySetup then
PlotPB(High,Low, "DMSeq.",green);
end;
buyIntersectBarNum = barNumber;
Value1 = MyColors("Orange");
Value2 = iff(High[1] < minLow1,2,1);
if PlotBuySetup then
PlotPB[value2](High[value2],Low[value2], "DMSeq.",value1); // paint intersection
end;
end;
Intersection

The Devil is in the Details

Counting bars is challenging. Because our FSM doesn’t fall through states after a transition, once the consecutive nine-bar sequence is complete, we’re already at the next bar—the tenth. To determine whether the high of the ninth bar exceeds the lows from earlier bars, I compare the high of bar 9 (denoted as high[1]) with the lowest low from three bars earlier (low[4]) across a range covering five bars. If necessary, I repeat a similar comparison, starting with the high of bar 8, using high[2] and the corresponding lows starting from low[5] over a five-bar range.   There is a chance neither bar will fulfill the criteria.  In this case we start forward scanning to see if high[1] fulfills the criteria.  Could we check for high[0] if the bar 8 and bar 9 fail?  You might be able to – it might be worth investigating. However, it will add more code.  Given some time you’ll see that high[1] will form an intersection. For clarity, I introduced the term Focus Bar to refer to the bar at index [0] in the historical data. In this context, if high[1] represents the prior bar relative to the Focus Bar and meets the intersection criteria, we need to immediately assess the Focus Bar to determine if it signals the start of the Countdown phase. Why act now? Because, without fall-through in our FSM, once a state transition occurs, the Focus Bar, if examined in the next state, would be skipped—so it’s essential to evaluate the Focus Bar (today’s bar if last bar on chart) right away while we are in the current state.  If close[0] < low[2], the bar is painted with the Countdown theme color and this phase begins and the state machine transitions

FSM State 2 – Looking for a completion of Countdown

We are almost there.  All we need are 13 bars that fulfill this criteria, close[0] < low[2].  From this point on we will be forward scanning and counting bars that fit the previously mentioned criteria.  Once we reach 13, we are done.  Finally, our journey to program the Sequential has come to an end.  Or has it?  So far, the description of the pattern has been straightforward, and we are in the home stretch?   The completion of 13 bars who’s close[0] < low[2] can take many days to complete and many things can happen during this time.   In his book, Tom Demark mentions three things that can derail the completion of the Countdown phase.  This is where the fun really begins.

	Case 2:
// Sell Countdown Phase
if close <= Low[2] then
begin
CountdownCountBuy = CountdownCountBuy + 1;
if PlotBuySetup then
PlotPB(High,Low, "DMSeq.",green); // Generate a buy signal
end;
if CountdownCountBuy = ctTarg then
begin
if PlotBuySetup then
begin
value99 = Text_new(d,t,low - range*0.2,"B");
PlotPB(High,Low, "DMSeq.",red); // Generate a buy signal
end;
resetBuyFSM();
end;
Portion of FSM State 2 that looks for and paints Countdown bars

This code is very simple – paint the bar if close[0] < low[2] and then count the bar.  Once the number of bars = ctTarg (CountDown Target). then place the letter “B” above the high and paint the bar a different color.  Sequential is now complete and so the Buy Finite State Machine needs to be reset.  Bar are we really done?

Countdown derailment

  • If a close exceeds the highest high during Setup – restart the process from scratch – reset the FSM
  • If a Sell Setup completes while waiting for the 13 bars – cancel the Buy Countdown and start the Sell Countdown
  • If a fresh Buy Setup reveals itself, recycle and restart the Countdown process.
       // Invalidate buy countdown if 
// 1.) a close > high during setup
// 2.) a sell setup occurs
// 3.) recycling occurs - new buy setup
if c > SetupHigh then
resetBuyFSM();
if stateSell = 2 then
resetBuyFSM();
if (seqBuySetup(suTarg) and barNumber - buyIntersectBarNum > suTarg) then
begin
stateBuy = 1;
SetupCountBuy = suTarg;
CountdownCountBuy = 0;
if plotBuySetUp then
paintBuySetup(suTarg);
SetUpHigh = highest(h,suTarg);
end;
2ND half of FSM State 2

The Dueling Nature of this Pattern Recognition Tool often prevents Sequential from reaching Completion – maybe a good thing.

Well, that is part of the reason.  The recycle of the Setup is also a culprit.  Because the pattern for the Buy and Sell requires the consumption of so many bars, the two FSM must run independent of each other and at times contradict each other.  Here is as good example of the FSMs taking action in the recent (April 2025) GOLD market.  Click on images to expand.

Wow! What a great buy!
It started out really great, but like any Trend Following approach…
Months to Complete

Sequential™ Examples

Shampoo, Rinse, Repeat.  Bingo!

We had 4 complete buy Setups with interrupted Countdowns

We had 4 complete buy Setups with interrupted Countdowns before it finally stuck!

Count-Downus Interruptus

Blow off top with Buy Setup completion interrupts the short Countdown.

Sell Countdown interrupted by Buy Setup

Market congestion, just like smoking, stunts growth of patterns.

Congestion Phase validated by incomplete Sequential Setups and Countdowns

Many Complex Patterns can be Programmed by using Multiple FSMs

Almost anything can be programmed with EasyLanguage and the concept of a Finite State Machine and the Switch-Case structure.  If you don’t know where to start, ask ChatGPT with the best and most descriptive prompt you can come up with.  Then start small and build up – always check your progress before moving on to the next phase.  Email me with any questions and if you like this type of content check out my books at Amazon.

George’s Amazon Author Page