Category Archives: EasyLanguage Indicator

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

Some EasyLanguage Functions Are Really “Classy”

The Series Function is very special

When a function accesses a previously stored value of one of its own variables, it essentially becomes a “series” function. Few programming languages offer this convenience out of the box. The ability to automatically remember a function’s internal state from one bar (or time step) to the next is known as state retention. In languages like Python, achieving this typically requires using a class structure. In Module #2 of my Easing into EasyLanguage Academy, I explained why EasyLanguage isn’t as simple as its name implies—yet this feature shows how its developers aimed to simplify otherwise complex tasks.

If you’ve ever worked with functions with memory that exchange data using a numericRef input, you’ve essentially been using a pseudo-class—a form of object-oriented programming in its own right. EasyLanguage includes a robust object-oriented library (with excellent resources by Sunny Harris and Sam Tennis – buy their book on Amazon), yet you’re confined to its built-in functionality since it doesn’t yet support user-defined class structures. Nonetheless, the series functionality combined with data passing brings you remarkably close to a true object-oriented approach—and the best part is, you might not even realize it.

Example of a Series Function

I was recently working on the Trend Strength indicator/function and have been mulling this post over for some time, so I thought this would be a good time to write about it.  The following indicator to function conversion will create a function of type series (a function with a memory.)  The name series is very appropriate in that this type of function runs along with the time series of the chart.  It must do this so it can reference prior bar values.

You can ensure a function is treated as a series function in two ways:

  1. Using a Prior Value:
    When you reference a previous value within the function, EasyLanguage automatically recognizes the need to remember past data and treats the function as a series function.

  2. Setting the Series Property:
    Alternatively, you can explicitly set the function’s property to “series” via a dialog. This instructs EasyLanguage to handle the function as a series function, ensuring that state is maintained across bars.

    Manually Set the Function to type Series

Importantly, regardless of the function’s name or even if it’s called within control structures (like an if construct), a series function is evaluated on every bar. This guarantees that the historical data is consistently updated and maintained, which is essential for accurate time series analysis.

Converting an indicator to a function

You can find a wide variety of EasyLanguage indicators online, though many are available solely as indicators. This is fine if you’re only interested in plotting the values. However, if you want to incorporate an indicator into a trading strategy, you’ll need to convert it into a function. For calculation-intensive indicators, it’s best to follow a standard prototype: use inputs for interfacing, perform calculations via function calls, and apply the appropriate plotting mechanisms. By adhering to this development protocol, your indicator functions can be reused across different analysis studies, enhancing encapsulation and modularity. Fortunately, converting an indicator’s calculations into a function is a relatively straightforward process. Here is indicator that I found somewhere.

Inputs: 
ShortLength(13), // Shorter EMA length
LongLength(25), // Longer EMA length
SignalSmoothing(7); // Smoothing for signal line
Vars:
DoubleSmoothedPC(0),
DoubleSmoothedAbsPC(0),
TSIValue(0),
SignalLine(0);
// Price Change (PC)
Value1 = Close - Close[1];
// First Smoothing (EMA of PC and |PC|)
Value2 = XAverage(Value1, LongLength);
Value3 = XAverage(AbsValue(Value1), LongLength);
// Second Smoothing (EMA of First Smoothed Values)
DoubleSmoothedPC = XAverage(Value2, ShortLength);
DoubleSmoothedAbsPC = XAverage(Value3, ShortLength);
// Compute TSI
If DoubleSmoothedAbsPC <> 0 Then
TSIValue = 100 * (DoubleSmoothedPC / DoubleSmoothedAbsPC);

// Compute Signal Line
SignalLine = XAverage(TSIValue, SignalSmoothing);
// Plot the TSI and Signal Line
Plot1(TSIValue, "TSI");
Plot2(SignalLine, "Signal");
TSI via Web
Now we can functionize it.

Below is an example of how you might convert an indicator into a function called TrendStrengthIndex. Notice that the first change is to replace any hard-coded numbers in the indicator’s inputs with parameters declared as numericSimple (or numericSeries where appropriate). This allows the function to accept dynamic values when called.  Not to give anything away, but you can also declare variables as numericRef, numericSeries, numericArrayRef, string, stringRef, and stringArrayRef.  Let’s not worry about these types right now.

inputs: 
ShortLength(numericSimple), // Shorter EMA length
LongLength(numericSimple), // Longer EMA length
SignalSmoothing(numericSimple); // Smoothing for signal line
Inputs must be changed to function nomenclature

Below is an example function conversion for the TrendStrengthIndex indicator.  The plot statements have been commented out since—rather than plotting—the function now passes back the calculated value to the calling program.

Vars:
DoubleSmoothedPC(0),
DoubleSmoothedAbsPC(0),
TSIValue(0),
SignalLine(0);
// Price Change (PC)
Value1 = Close - Close[1];
// First Smoothing (EMA of PC and |PC|)
Value2 = XAverage(Value1, LongLength);
Value3 = XAverage(AbsValue(Value1), LongLength);
// Second Smoothing (EMA of First Smoothed Values)
DoubleSmoothedPC = XAverage(Value2, ShortLength);
DoubleSmoothedAbsPC = XAverage(Value3, ShortLength);
// Compute TSI
If DoubleSmoothedAbsPC <> 0 Then
TSIValue = 100 * (DoubleSmoothedPC / DoubleSmoothedAbsPC);

// Compute Signal Line
SignalLine = XAverage(TSIValue, SignalSmoothing);
// Plot the TSI and Signal Line
//Plot1(TSIValue, "TSI"); commented out
//Plot2(SignalLine, "Signal"); commented out
TrendSTrengthIndex = TSIValue;
Functionalize It!

This works great if we just want the TrendStrengthIndex, but this indicator, like many others has a signal line.  The signal line for such indicators is usually a smoothed version of the main calculation.  Now you could do this smoothing outside the function, but wouldn’t it be easier if we did everything inside of the function?

Oh no!  I need to pass more than one value back!

If we just wanted to pass back TSIValue all we need to do is assign the name of the function to this value.

Passing values by reference

We can adjust the function to return multiple values by defining some of the inputs as numericRef. Essentially, when you pass a variable as a numericRef, you’re actually handing over its memory address—okay, let’s get nerdy for a moment! This means that when the function updates the value at that address, the calling routine immediately sees the change, giving the variable a kind of quasi-global behavior. Without numericRef, any modifications made inside the function stay local and never propagate back to the caller.  Not only is the function communicating with the calling strategy or indicator it is also remember its own stuff for future use.  Take a look at this code.

inputs: 
ShortLength(numericSimple), // Shorter EMA length
LongLength(numericSimple), // Longer EMA length
SignalSmoothing(numericSimple), // Smoothing for signal line

TrendStrength.index(numericRef), // Output
TrendStrength.signal(numericRef); //OutPut
Vars:
DoubleSmoothedPC(0),
DoubleSmoothedAbsPC(0),
SignalLine(0);

// Force series FUNCTION BEHAVIOR
Value4 = Value3[1];
// Price Change (PC)
Value1 = Close - Close[1];
// First Smoothing (EMA of PC and |PC|)
Value2 = XAverage(Value1, LongLength);
Value3 = XAverage(AbsValue(Value1), LongLength);

// Second Smoothing (EMA of First Smoothed Values)
DoubleSmoothedPC = XAverage(Value2, ShortLength);
DoubleSmoothedAbsPC = XAverage(Value3, ShortLength);
// Compute TSI
If DoubleSmoothedAbsPC <> 0 Then
TrendStrength.index = 100 * (DoubleSmoothedPC / DoubleSmoothedAbsPC);

// Compute Signal Line
TrendStrength.signal = XAverage(TrendStrength.index, SignalSmoothing);

TrendStrengthIndex = 1;
Is this a function or is it a class?

There is a lot going on here.  Since we are storing our calculations in the two numericRef inputs, TrendStrength.index and TrendStrength.signal the function name can simply be assigned the number 1.  You only need to do this because the function needs to be assigned something, or you will get a syntax error.  Since we are talking objects, I think it would be appropriate to introduce “dot notation.”  When programming with objects you access the class members and methods buy using a dot.  If you have an exponential moving average class in python you would access the variables and functions (methods) in the class like this.

class ExponentialMovingAverage:
# Class-level defaults serve as initial values.
alpha = 0.2 # Default smoothing factor
ema = None # EMA starts as None

def update(self, price):
"""
Update the EMA with a new price.

Parameters:
price (float): The new price to incorporate.

Returns:
float: The updated EMA value.
"""
# If ema is None, this is the first update
if self.ema is None:
self.ema = price
else:
self.ema = self.alpha * price + (1 - self.alpha) * self.ema
return self.ema

# Create an instance of the class.
ema_calculator = ExponentialMovingAverage()

# Dot notation to access the class attribute.
print("Alpha value:", ema_calculator.alpha)

# Dot notation to access the EMA attribute before any updates.
print("Initial EMA (should be None):", ema_calculator.ema)

# Dot notation to call the update method.
ema_value = ema_calculator.update(10)
print("EMA after update with 10:", ema_value)
Using dot notation to extract values from a class

Since you are using EasyLanguage and a series function, you don’t have to deal with something like this.  On the surface this looks a little gross but coming from a programming background this is quite eloquent.  I only show this to demonstrate dot notation.  In an attempt to mimic dot notation in the EasyLanguage function, I simply add a period ” , ” to the input variable names that will return the numbers we need to plot.  Take a look at the nomenclature I am using.

    TrendStrength.index(numericRef), // Output
TrendStrength.signal(numericRef); //OutPut
Function Name +

I am using the function name a ” . ” and an appropriate variable name.  This is not necessary.  Historically, input names that were modified within a function were preceded by the letter “O.”  In this example, Oindex and Osignal.  This represented “output.”  Remember these naming conventions are all up to you.  Here is the new indicator using our EasyLanguage “Classy” function and our pseudo dot notation nomenclature.

//Utilize the TrendStrengthIndex classsy function

inputs: shortLength1(9), longLength1(19), signalSmoothing1(9);
inputs: shortLength2(19), longLength2(39), signalSmoothing2(13);

vars: trendStrength1.index(0), trendStrength1.signal(0);
vars: trendStrength2.index(0), trendStrength2.signal(0);
value1 = TrendStrengthIndex(shortLength1,longLength1,signalSmoothing1,trendStrength1.index,trendStrength1.signal);
value2 = TrendStrengthIndex(shortLength2,longLength2,signalSmoothing2,trendStrength2.index,trendStrength2.signal);


plot1(trendStrength1.index,"TS-Index-1");
plot2(trendStrength1.signal,"TS-Signal-1");

plot3(trendStrength2.index,"TS-Index-2");
plot4(trendStrength2.signal,"TS-Signal-2");
Take a look at how we access the information we need from the function calls.

You might be surprised to learn that you may have been doing object-oriented programming all along without realizing it. Do you prefer the clarity of dot notation for accessing function output, or would you rather stick with a notation that uses a big “O” combined with the input name to represent functions with multiple outputs? Also, notice how each function call behaves like a new instance—the internal values remain discrete, meaning that each call remembers its own state independently.  In other words, each function call remembers its own stuff.

Two distinct function values from the same function – called twice on the same bar.

RMI Trend Sniper in EasyLanguage

RMI Trend Sniper Indicator – Described on ProRealTime in PR Code.

RMI Trend Sniper Indicator – Indicators – ProRealTime

RMI Trend Sniper: An Innovative Trading Indicator

The following is from the RealCode website – I have just copied and pasted this here.  Here is the header information that provides credit to the original programmer.

//PRC_RMI Trend Sniper
//version = 0
//26.03.24
//Iván González @ www.prorealcode.com
//Sharing ProRealTime knowledge

Here is the description of the Indicator via ProRealCode.  Please check out the website for further information regarding the indicator and how to use it.

The RMI Trend Sniper indicator is designed to identify market trends and trading signals with remarkable precision.

This tool combines the analysis of the Relative Strength Index (RSI) with the Money Flow Index (MFI) and a unique approach to range-weighted moving average to offer a comprehensive perspective on market dynamics.

Configuration and Indicator Parameters

The RMI Trend Sniper allows users to adjust various parameters according to their trading needs, including:

  • RMI Length: Defines the calculation period for the RMI.
  • Positive and Negative Momentum (Positive above / Negative below): Sets thresholds to determine the strength of bullish and bearish trends.
  • Range MA Visualization (Show Range MA): Enables users to visualize the range-weighted moving average, along with color indications to quickly identify the current market trend.
Cool Shading – right?

Many of my clients ask me to convert indicators from different languages.  One of my clients came across this from ProRealCode and asked me to convert for his MulitCharts.  Pro Real code is very similar to EasyLanguage with a few exceptions.  If you are savvy in EL, then I think you could pick up PRC quite easily.  Here it is.  It is a trend following indicator.  It is one of a few that I could not find the Easylanguage equivalent so I thought I would provide it.  Play around with it and let me know what you think.  Again, all credit goes to:

//Iván González @ www.prorealcode.com
//Sharing ProRealTime knowledge
 


inputs:Length(14),//RMI Length
pmom(66),//Positive above
nmom(30);//Negative below

//-----RSI and MFI calculation-----------------------------//

vars: alpha(0),src1(0),src2(0),up(0),down(0),myrsi(0),seed(True);

alpha = 1/length;
//-----Up
src1 = maxList(close-close[1],0);
if seed then
up = average(src1,length)
else
up = alpha*src1 + (1-alpha)*up[1];

//-----Down
src2 = -1 * minList(close-close[1],0);
if seed then
down = average(src2,length)
else
down = alpha*src2 + (1-alpha)*down[1];

seed = False;

//-----Rsi
if down = 0 then
myrsi = 100
else if up = 0 then
myrsi = 0
else
myrsi = 100 - (100/(1+up/down));
vars: mfiVal(0),rsimfi(0),bpmom(False),bnmom(False),positive(0),negative(0),ema(0);
//-----MFI
mfiVal = moneyFlow(length);
//-----RsiMfi
rsimfi = (myrsi+mfiVal)/2;
//----------------------------------------------------------//
//-----Long Short Conditions--------------------------------//
ema = average(c,5);

bpmom = rsimfi[1] < pmom and rsimfi > pmom and rsimfi > nmom and (ema-ema[1])>0;
bnmom = rsimfi<nmom and (ema-ema[1])<0;

if bpmom then
begin
positive = 1;
negative = 0;
end
else if bnmom then
begin
positive = 0;
negative = 1;
end;

//----------------------------------------------------------//
//------Calculate RWMA--------------------------------------//
vars: band(0),band20(0),barRange(0),weight(0),sum(0),twVal(0),rwma(0);

band = minList(avgtruerange(30)*0.3,close*(0.3/100));
band20 = band[20]/2*8;
barRange = high-low;

weight = BarRange/summation(BarRange,20);
sum = summation(close*weight,20);
twVal = summation(weight,20);
rwma = sum/twVal;

vars: r(0),g(0),b(0);

if positive = 1 then
begin
rwma = rwma-band;
r=0;
g=188;
b=212;
end
else if negative = 1 then
begin
rwma = rwma+band;
r=255;
g=82;
b=82;
end
else
rwma = 0;

//------------------------------------------------------------//
//-----Calculate MA bands-------------------------------------//
vars: mitop(0),mibot(0);

mitop = rwma+band20;
mibot = rwma-band20;


plot1(mitop,"TOP");
plot2((mitop+mibot)/2,"TOP-BOT");
plot3((mitop+mibot)/2,"BOT-TOP");
plot4(mibot,"BOT");
if positive = 1 then
begin
plot5(rwma,"Pos",GREEN);
noPlot(plot4);
end;
if negative =1 then
begin
plot6(rwma,"Neg",RED);
noPlot(plot3);
end;
Ignore the RGB Color Codes

 

Getting Creative to Shade Between Points on the Chart

TradeStation doesn’t provide an easy method to do shading, so you have to get a little creative.  The plot TOP is of type Bar High with the thickest line possible.  The plot TOP-BOT (bottom of top) is of type Bar Low.  I like to increase transparency as much as possible to see what lies beneath the shading . The BOT-TOP (top of bottom) is Bar High and BOT is Bar Low.  Pos and Neg are of type Point.  I have colored them to be GREEN or RED.

Indicator Settings.

Happy New Year!

Should you use a profit taking algorithm in your Trend Following system?

If letting profits run is key to the success of a trend following approach, is there a way to take profit without diminishing returns?

Most trend following approaches win less than 40% of the time.   So, the big profitable trades are what saves the day for this type of trading approach.  However, it is pure pain to simply sit there and watch a large profit erode, just because the criteria to exit the trade takes many days to be met.

Three methods to take a profit on a Trend Following algorithm

  1.  Simple profit objective – take a profit at a multiple of market risk.
  2.  Trail a stop (% of ATR) after a profit level (% of ATR) is achieved.
  3. Trail a stop (Donchian Channel) after a profit level (% of ATR) is achieved.

Use an input switch to determine which exit to incorporate

Inputs: initCapital(200000),rskAmt(.02),
useMoneyManagement(False),exitLen(13),
maxTradeLoss$(2500),
// the following allows the user to pick
// which exit to use
// 1: pure profit objective
// exit1ProfATRMult allows use to select
// amount of profit in terms of ATR
// 2: trailing stop 1 - the user can choose
// the treshhold amount in terms of ATR
// to be reached before trailing begins
// 3: trailing stop 2 - the user can chose
// the threshold amount in terms of ATR
// to be reached before tailing begins
whichExit(1),
exit1ProfATRMult(3),
exit2ThreshATRMult(2),exit2TrailATRMult(1),
exit3ThreshATRMult(2),exit3ChanDays(5);
Exit switch and the parameters needed for each switch.

The switch determines which exit to use later in the code.  Using inputs to allow the user to change via the interface also allows us to use an optimizer to search for the best combination of inputs.  I used MultiCharts Portfolio Trader to optimize across a basket of 21 diverse markets.  Here are the values I used for each exit switch.

MR = Market risk was defined as 2 X avgTrueRange(15).

  • Pure profit objective -Multiple from 2 to 10 in increments of 0.25.  Take profit at entryPrice + or – Profit Multiple X MR
  • Trailing stop using MR – Profit Thresh Multiple from 2 to 4 in increments of 0.1.  Trailing Stop Multiple from 1 to 4 in increments of 0.1.
  • Trailing stop using MR and Donchian Channel – Profit Thresh Multiple from 2 to 4 in increments of 0.1.  Donchian length from 3 to 10 days.

Complete strategy code incorporating exit switch.  This code is from Michael Covel’s 2005 Trend Following book (Covel, Michael. Trend Following: How Great Traders Make Millions in Up or Down Markets. FT Press, 2005.)  This strategy is highlighted in my latest installment in my Easing into EasyLanguage series – Trend Following edition.


vars:buyLevel(0),shortLevel(0),longExit(0),shortExit(0);

Inputs: initCapital(200000),rskAmt(.02),
useMoneyManagement(False),exitLen(13),
maxTradeLoss$(2500),whichExit(1),
exit1ProfATRMult(3),
exit2ThreshATRMult(2),exit2TrailATRMult(1),
exit3ThreshATRMult(2),exit3ChanDays(5);

Vars: marketRisk(0), workingCapital(0),
marketRisk1(0),marketRisk2(0),
numContracts1(0),numContracts2(0);

//Reinvest profits? - uncomment the first line and comment out the second
//workingCapital = Portfolio_Equity-Portfolio_OpenPositionProfit;
workingCapital = initCapital;


buyLevel = highest(High,89) + minMove/priceScale;
shortLevel = lowest(Low,89) - minMove/priceScale;
longExit = lowest(Low,exitLen) - minMove/priceScale;
shortExit = highest(High,exitLen) + minMove/priceScale;

marketRisk = avgTrueRange(15)*2*BigPointValue;
marketRisk1 =(buyLevel - longExit)*BigPointValue;
marketRisk2 =(shortExit - shortLevel)*BigPointValue;
marketRisk1 = minList(marketRisk,marketRisk1);
marketRisk2 = minList(marketRisk,marketRisk2);

numContracts1 = (workingCapital * rskAmt) /marketRisk1;
numContracts2 = (workingCapital * rskAmt) /marketRisk2;

if not(useMoneyManagement) then
begin
numContracts1 = 1;
numContracts2 =1;
end;

numContracts1 = maxList(numContracts1,intPortion(numContracts1)); {Round down to the nearest whole number}
numContracts2 = MaxList(numContracts2,intPortion(numContracts1));


if c < buyLevel then buy numContracts1 contracts next bar at buyLevel stop;
if c > shortLevel then Sellshort numContracts2 contracts next bar at shortLevel stop;

buytocover next bar at shortExit stop;
Sell next bar at longExit stop;

vars: marketRiskPoints(0);
marketRiskPoints = marketRisk/bigPointValue;
if marketPosition = 1 then
begin
if whichExit = 1 then
sell("Lxit-1") next bar at entryPrice + exit1ProfATRMult * marketRiskPoints limit;
if whichExit = 2 then
if maxcontractprofit > (exit2ThreshATRMult * marketRiskPoints ) * bigPointValue then
sell("Lxit-2") next bar at entryPrice + maxContractProfit/bigPointValue - exit2TrailATRMult*marketRiskPoints stop;
if whichExit = 3 then
if maxcontractprofit > (exit3ThreshATRMult * marketRiskPoints ) * bigPointValue then
sell("Lxit-3") next bar at lowest(l,exit3ChanDays) stop;
end;

if marketPosition = -1 then
begin
if whichExit = 1 then
buyToCover("Sxit-1") next bar at entryPrice - exit1ProfATRMult * marketRiskPoints limit;
if whichExit = 2 then
if maxcontractprofit > (exit2ThreshATRMult * marketRiskPoints ) * bigPointValue then
buyToCover("Sxit-2") next bar at entryPrice - maxContractProfit/bigPointValue + exit2TrailATRMult*marketRiskPoints stop;
if whichExit = 3 then
if maxcontractprofit > (exit3ThreshATRMult * marketRiskPoints ) * bigPointValue then
buyToCover("Sxit-3") next bar at highest(h,exit3ChanDays) stop;
end;

setStopLoss(maxTradeLoss$);

Here’s the fun code from the complete listing.

vars: marketRiskPoints(0);
marketRiskPoints = marketRisk/bigPointValue;
if marketPosition = 1 then
begin
if whichExit = 1 then
sell("Lxit-1") next bar at entryPrice + exit1ProfATRMult * marketRiskPoints limit;
if whichExit = 2 then
if maxContractProfit > (exit2ThreshATRMult * marketRiskPoints ) * bigPointValue then
sell("Lxit-2") next bar at entryPrice + maxContractProfit/bigPointValue - exit2TrailATRMult*marketRiskPoints stop;
if whichExit = 3 then
if maxContractProfit > (exit3ThreshATRMult * marketRiskPoints ) * bigPointValue then
sell("Lxit-3") next bar at lowest(l,exit3ChanDays) stop;
end;

if marketPosition = -1 then
begin
if whichExit = 1 then
buyToCover("Sxit-1") next bar at entryPrice - exit1ProfATRMult * marketRiskPoints limit;
if whichExit = 2 then
if maxContractProfit > (exit2ThreshATRMult * marketRiskPoints ) * bigPointValue then
buyToCover("Sxit-2") next bar at entryPrice - maxContractProfit/bigPointValue + exit2TrailATRMult*marketRiskPoints stop;
if whichExit = 3 then
if maxContractProfit > (exit3ThreshATRMult * marketRiskPoints ) * bigPointValue then
buyToCover("Sxit-3") next bar at highest(h,exit3ChanDays) stop;
end;

The first exit is rather simple – just get out on a limit order at a nice profit level.  The second and third exit mechanisms are a little more complicated.  The key variable in the code is the maxContractProfit keyword.  This value stores the highest level, from a long side perspective, reached during the life of the trade.  If max profit exceeds the exit2ThreshATRMult, then trail the apex by exit2TrailATRMult.  Let’s take a look at the math from a long side trade.

if maxContractProfit > (exit2ThreshATRMult * marketRiskPoints ) * bigPointValue

Since maxContractProfit is in dollar you must convert the exit2ThreshATRMult X marketRiskPoints into dollars as well.  If you review the full code listing you will see that I convert the dollar value, marketRisk, into points and store the value in marketRiskPoints.  The conversion to dollars is accomplished by multiplying the product by bigPointValue.

sell("Lxit-2") next bar at
entryPrice + maxContractProfit / bigPointValue - exit2TrailATRMult * marketRiskPoints stop;

I know this looks complicated, so let’s break it down.  Once I exceed a certain profit level, I calculate a trailing stop at the entryPrice plus the apex in price during the trade (maxContractProfit / bigPointValue) minus the exit2TrailATRMult X marketRiskPoints. If the price of the market keeps rising, so will the trailing stop.  That last statement is not necessarily true, since the trailing stop is based on market volatility in terms of the ATR.  If the market rises a slight amount, and the ATR increases more dramatically, then the trailing stop could actually move down.  This might be what you want.  Give the market more room in a noisier market.  What could you do to ratchet this stop?  Mind your dollars and your points in your calculations.

The third exit uses the same profit trigger, but simply installs an exit based on a shorter term Donchian channel.  This is a trailing stop too, but it utilizes a chart point to help define the exit price.

Results of the three exits

Exit 1 – Pure Profit Objective

Take a profit on a limit order once profit reaches a multiple of market risk aka 2 X ATR(15).

Pure profit object. Profit in terms of ATR or perceived market risk.

The profit objective that proved to be the best was using a multiple of 7.  A multiple of 10 basically negates the profit objective.   With this system several profit objective multiples seemed to work.

Exit – 2 – Profit Threshold and Trailing Stop in terms of ATR or market risk

Trail a stop a multiple of ATR after a multiple of ATR in profit is reached.

Trailing Stop using ATR
3-D view of parameters
3D view of parameter pairs

This strategy liked 3 multiples of ATR of profit before trailing and applying a multiple of 1.3 ATR as a stop.

Like I said in the video, watch out for 1.3 as you trailing amount multiple as it seems to be on a mountain ridge.

Exit – 3 – Profit Threshold in terms of ATR or market risk and a Donchain Channel trailing stop

Trail a stop using a Donchian Channel after a multiple of ATR in profit is reached.  Here was a profit level is reached, incorporate a tailing stop at the lowest low or the highest high of N days back.

Using Donchian Channel as trailing stop.
3-D view of parameters
3D view of parameters for Exit 3.

Conclusion

The core strategy is just an 89-day Donchian Channel for entry and a 13-Day Donchian Channel for exit.  The existing exit is a trailing exit and after I wrote this lengthy post, I started to think that a different strategy might be more appropriate.  However, as you can see from the contour charts, using a trailing stop that is closer than a 13-day Donchian might be more productive.   From this analysis you would be led to believe the ATR based profit and exit triggers (Exit #2) is superior.  But this may not be the case for all strategies.  I will leave this up to you to decide.  Here is the benchmark performance analysis with just the core logic.

Core logic results.

If you like this type of explanation and code, make sure you check at my latest book at amazon.com.  Easing into EasyLanguage – Trend Following Edition.

How to Fix the Fixed Fractional Position Size

The Fixed Fractional position sizing scheme is the most popular, so why does it need fixed?

Problems solved with Fixed Fractional:

  1. Efficient usage of trading capital
  2. Trade size normalization between different futures contracts
  3. Trade size normalization across different market environments

These are very good reasons why you should use positions sizing.  Problem #2 doesn’t apply if you are only trading one market.  This sounds idyllic, right?  It solves these two problems, but it introduces a rather bothersome side effect – huge draw down.  Well, huge draw down in absolute terms.  Draw downs when using a fixed fractional approach are proportional to the prior run up.  If you make a ton of money on a prior trade, then your position sizing reflects that big blip in the equity curve.  So, if you have a large loser following a large winner, the draw down will be a function of the run up.  In most cases, a winning trading system using fixed fractional position sizing will scale profit up as well as draw down.  A professional money manager will look at the profit to draw down ratio instead of the absolute draw down value.  The efficient use of capital will reflect a smaller position size after a draw down, so that is good right?  It is unless you believe in a Martingale betting algorithm – double up on losses and halve winners.  Are we just stuck with large “absolute” draw downs when using this size scheme?

Possible solutions to fixing Fixed Fractional (FF)

The first thing you can do is risk less than the industry standard 2% per trade.  Using 1% will cause equity to grow at a slower rate and also reduce the inevitable draw down.  But this doesn’t really solve the problem as we are giving up the upside.  And that might be okay with you.  Profit will increase and you are using an algorithm for size normalization.  In this blog I am going to propose a trading equity adjustment feature while using FF.  What if we act like money managers, and you should even if you are trading your own personal money, and at the end of the year or month we take a little off the table (theoretically – we are not removing funds from the account just from the position sizing calculation) that is if there is any extra on the table.  This way we are getting the benefit of FF while removing a portion of the compounding effect, which reduces our allocation for the next time interval.  How do you program such a thing?  Well first off let’s code up the FF scheme.

positionSize = round((tradingCapital * riskPerTrade) / (avgTrueRange(30)*bigPointValue),0);

Nothing new here.  Simply multiply tradingCapital by the riskPerTrade (1 or 2%) and then divide by a formula that defines current and inherent market risk.  This is where you can become very creative.  You could risk the distance between entry and exit if you know those values ahead of time or you can use a value in terms of the current market.  Here I have chosen the 30-day average true range.  This value gives a value that predicts the market movement into the future.  However, this value only gives the expected market movement for a short period of time into the future.  You could us a multiplier since you will probably remain in a trade for more than a few days – that is if you are trying to capture the trend.  In my experiment I just use one as my multiplier.

Capture and store the prior year/month NetProfit

When I come up with a trading idea I usually just jump in and program it.  I don’t usually take time to see if Easy Language already provides a solution for my problem.   Many hours have been used to reinvent the wheel, which isn’t always a bad thing.  So, I try to take time and search the functions to see if the wheel already exists.  This time it looks like I need to create the wheel.  I will show the code first and then explain afterward.

inputs: useAllocateYearly(True),useAllocateMonthly(False),initCapital(100000),removePerProfit(0.50),riskPerTrade(0.01);
vars: tradingCapital(0),prevNetProfit(0),tradingCapitalAdjustment(0);
vars: oLRSlope(0),oLRAngle(0),oLRIntercept(0), oLRValueRaw(0),mp(0);
arrays: yearProfit[250](0),snapShotNetProfit[250](0);vars: ypIndex(0);


once
begin
tradingCapital = initCapital;
end;

if useAllocateYearly then
begin
value1 = year(d);
value2 = year(d[1]);
end;

if useAllocateMonthly then //remember make sure your array is 12XNumYears
begin
value1 = month(d);
value2 = month(d[1]);
end;

if useAllocateYearly or useAllocateMonthly then
begin
if value1 <> value2 then
begin
if ypIndex > 0 then
yearProfit[ypIndex] = prevNetProfit - snapShotNetProfit[ypIndex-1]
else
yearProfit[ypIndex] = prevNetProfit;

snapShotNetProfit[ypIndex] = prevNetProfit;
tradingCapitalAdjustment = yearProfit[ypIndex];
if yearProfit[ypIndex] > 0 then
tradingCapitalAdjustment = yearProfit[ypIndex] * (1-removePerProfit);
tradingCapital = tradingCapital + tradingCapitalAdjustment;
print(d,",",netProfit,",",yearProfit[ypIndex],",",tradingCapitalAdjustment,",",tradingCapital);
ypIndex +=1;
end;
end
else
tradingCapital = initCapital + netProfit;
Capture either the prior years or months net profit

I wanted to test the idea of profit retention on a monthly and yearly basis to see if it made a difference.  I also wanted to just use the vanilla version of FF.  The use of Arrays may not be necessary, but I didn’t know ahead of time.  When you program on the fly, which is also called “ad hoc” programming you create first and then refine later.  Many times, the “ad hoc” version turns out to be the best approach but may not be the most efficient.  Like writing a book, many times your code needs revisions.  When applying a study or strategy that uses dates to a chart, you don’t know exactly when the data starts so you always need to assume you are starting in the middle of a year.   If you are storing yearly data into an array, make sure you dimension you array sufficiently.  You will need 12X the number of years as the size you need to dimension your array if you want to store monthly data.

 


//250 element array will contain more than 20 years of monthly data
//You could increase these if you like just to be safe
arrays: yearProfit[250](0),snapShotNetProfit[250](0);
//Remember you dimension you arrray variable first and then
//Pass it a value that you want to initiate all the values
//in the array to equal
vars: ypIndex(0);
Dimension and Initiate Your Arrays

The first thing we need to do is capture the beginning of the year or month.  We can do this by using the year and month function.  If the current month or year value is not the same as the prior day’s month or year value, then we know we crossed the respective timeline boundary.  We are using two arrays, snapShotNetProfit and yearProfit (to save time I use this array to store monthlty values as well, when that option is chosen) and a single array index ypIndex.  If we have crossed the time boundary, the first thing we need to do is capture the EasyLanguage function NetProfit’s value.  NetProfit keeps track of the cumulative closed out trade profits and losses. Going forward in this description I am going to refer to a yearly reallocation.  If it’s the first year, the ypIndex will be zero, and in turn the first year’s profit will be the same as netProfit.  We store netProfit in the yearProfit array at the ypIndex location.  Since we are in a new year, we take a snapshot of netProfit and store it in the snapShotNetProfit array at the same ypIndex location.  You will notice I use the variable prevNetProfit in the code for netProfit.  Here is where the devil is in the details.  Since we are comparing today’s year value with yesterday’s year value and when they are different, we are already inside the new year, so we need to know yesterday’s netProfit.   Before you say it, you can’t pass netProfit a variable for prior values; you know like netProfit(1) or netProfit[1] – this is a function that has no historic values, but you can record the prior day’s value by using our own prevNetProfit  variable.  Now we need to calculate the tradingCapitalAdjustment.  The first thing we do is assign the variable the value held in  yearProfit[ypIndex].  We then test the yearProfit[ypIndex] value to see if it is positive.  If it is, then we multiply it by (1-removePerProfit).  If you want to take 75% of the prior year’s profit off the table, then you would multiply the prior year’s profit by 25%.  Let’s say you make $10,000 and you want to remove $7,500, then all you do is multiply $10,000 by 25%.  If the prior year’s netProfit is a loss, then this value flows directly through the code to the position sizing calculation (auto deallocation on a losing year).   If not, the adjusted profit portion of funds are deallocated in the position sizing equation.

The next time we encounter a new year, then we know this is the second year in our data stream, so we need to subtract last year’s snapshot of netProfit (or prevNetProfit) from the current netProfit.  This will give us the change in the yearly net profit.  We stuff this information into the yearProfit array.  The snapShotNetProfit is stuffed with the current prevNetProfit.  ypIndex is incremented every time we encounter a new yearNotice how I increment the ypIndex – it is incremented after all the calculations in the new year.  The tradingCapitalAdjustment is then calculated with the latest information.

Here is a table of how the tradingCapital and profit retention adjustment are calculated.  A yearly profit adjustment only takes place after a profitable year.  A losing year passes without adjustment.

All tests were carried out on the trend friendly crude oil futures with no execution costs from 2006 thru 2/28/2204.

See how money is removed from the allocation model after winning years.

Here are some optimization tests with 75% profit retention on yearly and monthly intervals.

Yearly First-

Yearly retention with no stop loss or break-even levels.

Now Monthly-

Monthly retention with no stop loss or break-even levels.

What if we didn’t reallocate on any specific interval?

Huge drawdowns with very little change in total profit.

Add some trade management into the mix.

Here we optimize a protective stop and a break-even level to see if we can juice the results.  All the trade management is on a position basis.

200K with No Reallocating with $14k and $8.5 stop/break-even levels [ranked by Profit Factor]
No position sizing and no reallocation with $5K and $10K stop/break-even levels

200K and monthly reallocating with $7K and $5.5K stop/break-even levels [BEST PROFIT FACTOR]
200K and monthly reallocating with $7K and $8.5K stop/break-even levels [2ND BEST PROFIT FACTOR]

Are we really using our capital in the most efficient manner?

If we retain profit, should we remove some of the loss form the position sizing engine as well.  All the tests I performed retained profit from the position size calculations.  I let the loss go full bore into the calculation.  This is a very risk averse approach.  Why don’t I retain 25% of losses and deduct that amount from the yearly loss and feed that into the position sizing engine.  This will be less risk averse – let’s see what it does.

Not as good.  But I could spend a week working different permutations and optimization sessions.

Are you wondering what Trend Following System I was using as the foundation of this strategy?  I used EasyLanguage’s Linear Regression function to create buy and short levels.  Here is the very simple code.

Value1 = LinearReg (Close, 60, 1, oLRSlope, oLRAngle, oLRIntercept, oLRValueRaw);

Value2 = oLRSlope;

Value3 = oLRAngle;

Value4 = oLRIntercept;

Value5 = oLRValueRaw;


//Basically I am buying/shorting on the change of the linear regression slope
//Also I have a volatility filter but it really isn't used
If value2 >=0 and value2[1] < 0 and avgTrueRange(30)*bigPointValue < 10000 then
buy positionSize contracts next bar at market;
If value2 <=0 and value2[1] > 0 and avgTrueRange(30)*bigPointValue < 10000 then
sellShort positionSize contracts next bar at market;

mp = marketPosition;

//I also have incorporated a 3XATR(30) disaster stop
if mp = 1 and c <= entryPrice - 3 * avgTrueRange(30) then sell next bar at market;
if mp = -1 and c >= entryPrice + 3 * avgTrueRange(30) then buyToCover next bar at market

If you want to see some more Trend Following models and their codes in EasyLanguage and TS-18 Python check out my TrendFollowing Guide and Kit.

Pyramania Levels with 24-Hour Session – Free Code

Easing Into EasyLanguage-DayTrade Edition [On SALE Now thru November]

Get Your Copy Now!  On sale now for thru the end of November!

EZ-DT Pyramania is a strategy I introduced in the Day Trade Edition.  The logic is rather simple – pyramid as the market moves through multiple levels during the trading day. – buy, buy, buy, dump or buy, dump, short, short, short, dump.  The distance between the levels is constant.  In the book, I showed an algorithm with a total of 6 levels with 7 edges.

Pyramania of @ES.D

Here the market opens @ 9:30 and the levels are instantly plotted and trades are executed as the market moves through the levels located above the open tick.  Over the weekend, I had a reader ask how he could modify the code to plot the levels on the 24-hour @ES session.  In the day session, I used the change in the date as the trigger for the calculation and plotting of the levels.  Here is the day session version.

inputs:numSegments(6),numPlots(6);

arrays: segmentBounds[](0);

variables: j(0),loopCnt(0),segmentSize(0),avgRng(0);
once
Begin
Array_SetMaxIndex(segmentBounds, numSegments);
end;

if d <> d[1] Then // only works on the @ES.D or any .D session
begin
avgRng = average(range of data2,20);
segmentSize = avgRng/numSegments;
loopCnt = -1*numSegments/2;
for j = 0 to numSegments
begin
segmentBounds[j] = openD(0) + loopCnt * segmentSize;
loopCnt = loopCnt + 1;
end;
end;

//The following time constraint only works when all time stamps
//are less than the end of day time stamp
//This will not work when time = 1800 and endTime = 1700
if t < calcTime(sessionEndTime(0,1),-barInterval) Then
begin
if numPlots >= 1 then plot1(segmentBounds[0],"Level 0");
if numPlots >= 2 then plot2(segmentBounds[1],"Level 1");
if numPlots >= 3 then plot3(segmentBounds[2],"Level 2");
if numPlots >= 4 then plot4(segmentBounds[3],"Level 3");
if numPlots >= 5 then plot5(segmentBounds[4],"Level 4");
if numPlots >= 6 then plot6(segmentBounds[5],"Level 5");
if numPlots >= 7 then plot7(segmentBounds[6],"Level 6");
// plot8(segmentBounds[7],"Level 7");
// plot9(segmentBounds[8],"Level 8");
end;
Works great with @ES.D or any @**.D

I like this code because it exposes you to arrays, loops, and plotting multiple values.  You can fix this by modifying and adding some code.  I used the Trading Around Midnight blog post to get the code I needed to enable plotting around 0:00 hours.  Here is the updated code:

inputs:numSegments(6),numPlots(6);
arrays: segmentBounds[](0);
variables: j(0),loopCnt(0),segmentSize(0),avgRng(0),
startTime(0),endTime(0),endTimeOffset(0);

once
Begin
Array_SetMaxIndex(segmentBounds, numSegments);
end;

startTime = sessionStartTime(0,1);
endTime = sessionEndTime(0,1);
//let TS tell you when the market opens - remember the
//first time stamp is the open time + bar interval
if t = calcTime(sessionStartTime(0,1),barInterval) Then
begin
avgRng = average(range of data2,20);
segmentSize = avgRng/numSegments;
loopCnt = -1*numSegments/2;
for j = 0 to numSegments
begin
segmentBounds[j] = open + loopCnt * segmentSize;
loopCnt = loopCnt + 1;
end;
end;

// if startTime > endTime then you know you are dealing with
// timees that more than likely bridges midnight
// if time is greater then 1700 (end time) then you must
// subtract an offset so it makes sense - endTimeOffset
// play with the math and it will come to you
if startTime > endTime then
begin
endTimeOffset = 0;
if t >= startTime+barInterval and t<= 2359 then
endTimeOffSet = 2400-endTime;
end;
if t-endTimeOffSet < endTime Then
begin
if numPlots >= 1 then plot1(segmentBounds[0],"Level 0");
if numPlots >= 2 then plot2(segmentBounds[1],"Level 1");
if numPlots >= 3 then plot3(segmentBounds[2],"Level 2");
if numPlots >= 4 then plot4(segmentBounds[3],"Level 3");
if numPlots >= 5 then plot5(segmentBounds[4],"Level 4");
if numPlots >= 6 then plot6(segmentBounds[5],"Level 5");
if numPlots >= 7 then plot7(segmentBounds[6],"Level 6");
// plot8(segmentBounds[7],"Level 7");
// plot9(segmentBounds[8],"Level 8");
end;
Modification to plot data around midnight

Here I let TS tell me with the market opens and then use some simple math to make sure I can plot with the time is greater than and less than the end of day time.

Plots from 18:05 through midnight until 17:00 the next day.

Email me if you have the book and want to companion code to the strategy – georgeppruitt@gmail.com

 

 

 

 

The Day Trading Edition of Easing Into EasyLanguage Now Available!

Learn Pyramiding, Scaling In or Out, Camarilla, Break Out and Clear Out techniques in day trading environment.

Get Your Copy Now!
  • Chapter 1 – Open Range Break Out and other Sundries
  • Chapter 2 – Improving Our ORBO with Pattern Recognition
  • Chapter 3 – Time based Breakout
  • Chapter 4 – Trading After the Lunch Break
  • Chapter 5 – Extending Our Break Out Technology With Pyramiding and Scaling Out
  • Chapter 6 – Scaling Out of Pyramided and multiple Positions
  • Chapter 7 – Pyramania
  • Chapter 8 – Introduction to Zone Trading with the Camarilla
  • Chapter 9 – Day Trading Templates Appendix
A Chart from Chapter 8 – Camarilla Equation

Take a look at this function that spans the entire search space of all the combinations of the days of the week.

inputs: optimizeNumber(numericSimple);
arrays: dowArr[31]("");
vars: currDOWStr(""),daysOfWeek("MTWRF");

//Once
//begin
print(d," ",t," assigning list ");
dowArr[1]="MTWRF";dowArr[2]="MTWR";
dowArr[3]="MTWF";dowArr[4]="MTRF";
dowArr[5]="MWRF";dowArr[6]="TWRF";
dowArr[7]="MTW";dowArr[8]="MTR";
dowArr[9]="MTF";dowArr[10]="MWR";
dowArr[11]="MWF";dowArr[12]="MRF";
dowArr[13]="TWR";dowArr[14]="TWF";
dowArr[15]="TRF";dowArr[16]="WRF";
dowArr[17]="MT";dowArr[18]="MW";
dowArr[19]="MR";dowArr[20]="MF";
dowArr[21]="TW";dowArr[22]="TR";
dowArr[23]="TF";dowArr[24]="WR";
dowArr[25]="WF";dowArr[26]="RF";
dowArr[27]="M";dowArr[28]="T";
dowArr[29]="W";dowArr[30]="R";
dowArr[31]="F";
//end;

OptimizeDaysOfWeek = False;
if optimizeNumber > 0 and optimizeNumber < 32 Then
Begin
currDOWStr = midStr(daysOfWeek,dayOfWeek(d),1);
if inStr(dowArr[optimizeNumber],currDOWStr) <> 0 Then
OptimizeDaysOfWeek = True;
end;
All The Permutations of the Days of the Week

 

Trade after the Lunch Break in Apple

Williams Awesome Oscillator Indicator and Strategy

Awesome Oscillator

This is a very simple yet telling analysis.  Here is the EasyLanguage:

[LegacyColorValue = true]; 



Vars:oscVal(0),mavDiff(0);


mavDiff= Average((h+l)/2,5)-Average((h+l)/2,34);
oscVal = mavDiff - average(mavDiff,5);

Plot3( 0, "ZeroLine" ) ;

if currentbar>=1 then
if oscVal>oscVal[1] then plot1(mavDiff,"+AO")
else plot2(mavDiff,"-AO")
Williams Awesome Oscillator Source Code

And here is what it looks like:

Williams Awesome Oscillator

The code reveals a value that oscillates around 0.  First calculate the difference between the 5-day moving average of the daily midPoint (H+ L)/2 and the 34-day moving average of the midPoint.    A positive value informs us that the market is in a bullish stance whereas a negative represents a bearish tone.  Basically, the positive value is simply stating the shorter-term moving average is above the longer term and vice versa. The second step in the indicator calculation is to subtract the 5-day moving average of the differences from the current difference.  If the second calculation is greater than the prior day’s calculation, then plot the original calculation value as green (AO+).  If it is less (A0-), then paint the first calculation red.  The color signifies the momentum between the current and the five-day smoothed value.

Awesome Indicator Strategy

mavDiff= Average((h+l)/2,5)-Average((h+l)/2,34);
oscVal = mavDiff - average(mavDiff,5);


mp = marketPosition;
mavUp = countIf(mavDiff > 0,30);
mavDn = countIf(mavDiff < 0,30);

value1 = countIf(oscVal > oscVal[1],10);

oscRatio = value1/10*100;
The beginning of an AO based Strategy

Here I am using the very handy countIf function.  This function will tell you how many times a Boolean comparison is true out of the last N days.  Her I use the function twice, but I could have replaced the second function call with mavDn = 30 – mavUp.  So, I am counting the number of occurrences of when the mavDiff is positive and negative over the past 30-days.  I also count the number of times the oscVal is greater than the prior oscVal.  In other words, I am counting the number of green bars.  I create a ratio between green bars and 10.  If there are six green bars, then the ratio equals 60% This indicates that the ratio of red bars would be 40%.  Based on these readings you can create trade entry directives.

if canShort and mavUp > numBarsAbove and mavDiff > minDiffAmt and oscRatio >= obRatio then
sellShort next bar at open;

if canBuy and mavDn > numBarsBelow and mavDiff < -1*minDiffAmt and oscRatio <= osRatio Then
buy next bar at open;
Trade Directives

If the number of readings out of the last 30 days is greater than numBarsAbove and mavDiff is of a certain magnitude and the oscillator ratio is greater than buyOSCRatio, then you can go short on the next open.  Here we are looking for the market to converge.  When these conditions are met then I think the market is overbought.  You can see how I set up the long entries.  As you can see from the chart it does a pretty good job.  Optimizing the parameters on the crude oil futures yielded this equity curve.

Too few trades!

Not bad, but not statistically significant either.  One way to generate more trades is to install some trade management such as protective stop and profit objective.

Using wide stop and profit objective.

Using a wide protective stop and large profit objective tripled the number of trades.  Don’t know if it is any better, but total performance was not derived from just a couple of trades.  When you are working with a strategy like this and overlay trade management you will often run into this situation.

Exiting while conditions to short are still turned on!

Here we either get stopped out or take a profit and immediately reenter the market.  This occurs when the conditions are still met to short when we exit a trade.  The fix for this is to determine when an exit has occurred and force the entry trigger to toggle off.  But you have to figure out how to turn the trigger back on.  I reset the triggers based on the number of days since the triggers were turned off – a simple fix for this post.  If you want to play with this strategy, you will probably need a better trigger reset.

I am using the setStopLoss and setProfitTarget functionality via their own strategies – Stop Loss and Profit Target.  These functions allow exit on the same as entry, which can be useful.  Since we are executing on the open of the bar, the market could definitely move either in the direction of the stop or the profit.  Since we are using wide values, the probability of both would be minimal.  So how do you determine when you have exited a trade.  You could look the current bar’s marketPosition and compare it with the prior bar’s value, but this doesn’t work 100% of the time.  We could be flat at yesterday’s close, enter long on today’s open and get stopped out during the day and yesterday’s marketPosition would be flat and today’s marketPosition would be flat as well.  It would be as if nothing occurred when in fact it did.

Take a look at this code and see if it makes sense to you.

if mp[1] = 1 and totalTrades > totTrades then
canBuy = False;

if mp[1] = -1 and totalTrades > totTrades then
canShort = False;

if mp[1] = 0 and totalTrades > totTrades then
Begin
if mavDiff[1] < 0 then canBuy = False;
if mavDiff[1] > 0 then canShort = False;
end;

totTrades = totalTrades;
Watch for a change in totalTrades.

If we were long yesterday and totalTrades (builtin keyword/function) increases above my own totTrades, then we know a trade was closed out – a long trade that is.  A closed out short position is handled in the same manner.  What about when yesterday’s position is flat and totalTrades increases.  This means an entry and exit occurred on the current bar.  You have to investigate whether the position was either long or short.  I know I can only go long when mavDiff is less than zero and can only go short when mavDiff is greater than zero.  So, all you need to do is investigate yesterday’s mavDiff  to help you determine what position was entered and exited on the same day.  After you determine if an exit occurred, you need to update totTrades with totalTrades.  Once you determine an exit occurred you turn canBuy or canShort off.  They can only be turned back on after N bars have transpired since they were turned off.   I use my own barsSince function to help determine this.

if  not(canBuy) Then
if barsSince(canBuy=True,100,1,0) = 6 then
canBuy = True;
if not(canShort) Then
if barsSince(canShort=True,100,1,0) = 6 then
canShort = True;

Complete strategy code:

inputs:numBarsAbove(10),numBarsBelow(10),buyOSCRatio(60),shortOSRatio(40),minDiffAmt(2),numBarsTrigReset(6);
Vars:canBuy(True),canShort(True),mp(0),totTrades(0),oscVal(0),mavDiff(0),oscRatio(0),mavUp(0),mavDn(0),stopLoss$(1000);


mavDiff= Average((h+l)/2,5)-Average((h+l)/2,34);
oscVal = mavDiff - average(mavDiff,5);


mp = marketPosition;
mavUp = countIf(mavDiff > 0,30);
mavDn = countIf(mavDiff < 0,30);

value1 = countIf(oscVal > oscVal[1],10);

oscRatio = value1/10*100;



if not(canBuy) Then
if barsSince(canBuy=True,100,1,0) = numBarsTrigReset then
canBuy = True;
if not(canShort) Then
if barsSince(canShort=True,100,1,0) = numBarsTrigReset then
canShort = True;

if mp[1] = 1 and totalTrades > totTrades then
canBuy = False;

if mp[1] = -1 and totalTrades > totTrades then
canShort = False;

if mp[1] = 0 and totalTrades > totTrades then
Begin
if mavDiff[1] < 0 then canBuy = False;
if mavDiff[1] > 0 then canShort = False;
end;


if canShort and mavUp > numBarsAbove and mavDiff > minDiffAmt and oscRatio >= buyOSCRatio then
sellShort next bar at open;

if canBuy and mavDn > numBarsBelow and mavDiff < -1*minDiffAmt and oscRatio <= shortOSRatio Then
buy next bar at open;

totTrades = totalTrades;
Inputs used to generate the equity curve.

Reentry of Long at better price

How to reenter at a better price after a stop loss

A reader of this blog wanted to know how he could reenter a position at a better price if the first attempt turned out to be a loser.  Here I am working with 5 minute bars, but the concepts work for daily bars too.  The daily bar is quite a bit easier to program.

ES.D Day Trade using 30 minute Break Out

Here we are going to wait for the first 30 minutes to develop a channel at the highest high and the lowest low of the first 30 minutes.  After the first 30 minutes and before 12:00 pm eastern, we will place a buy stop at the upper channel.  The initial stop for this initial entry will be the lower channel.  If we get stopped out, then we will reenter long at the midpoint between the upper and lower channel.  The exit for this second entry will be the lower channel minus the width of the upper and lower channel.

Here are some pix.  It kinda, sorta worked here – well the code worked perfectly.   The initial thrust blew through the top channel and then immediately consolidated, distributed and crashed through the bottom channel.  The bulls wanted another go at it, so they pushed it halfway to the midpoint and then immediately the bears came in and played tug of war.  In the end the bulls won out.

Initial Break Out Failed. But 2nd Entry made a little.

Here the bulls had initial control and then the bears, and then the bulls and finally the bears tipped the canoe over.

We gave it the old college try didn’t we fellas.

This type of logic can be applied to any daytrade or swing trade algorithm.  Here is the code for the strategy.

//working with 5 minute bars here
//should work with any time frame

input:startTradeTime(0930),numOfBarsHHLL(6),stopTradeTime(1200),maxLongEntriesToday(2);
vars: startTime(0),endTime(0),
barCountToday(0),totTrades(0),mp(0),longEntriesToday(0),
periodHigh(-99999999),periodLow(99999999);

startTime = sessionStartTime(0,1);
endTime = sessionStartTime(0,1);


if t = calcTime(startTime,barInterval) Then
begin
periodHigh = -99999999;
periodLow = 99999999;
barCountToday = 1;
totTrades = totalTrades;
end;

if barCountToday <= numOfBarsHHLL Then
Begin
periodHigh = maxList(periodHigh,h);
periodLow = minList(periodLow,l);
end;

barCountToday = barCountToday + 1;

longEntriesToday = totalTrades - totTrades;

mp = marketPosition;
if t <= stopTradeTime and barCountToday > numOfBarsHHLL Then
Begin
if longEntriesToday = 0 then
buy("InitBreakOut") next bar at periodHigh + minMove/priceScale stop;
if longEntriesToday = 1 Then
buy("BetterBuy") next bar at (periodHigh+periodLow)/2 stop;
end;

if mp = 1 then
if longEntriesToday = 0 then sell("InitXit") next bar at periodLow stop;
if longEntriesToday = 1 then sell("2ndXit") next bar at periodLow - (periodHigh - periodLow) stop;

SetExitOnClose;

The key concepts here are the time constraints and how I count bars to calculate the first 30 – minute channel.  I have had problems with EasyLanguages’s entriesToday function, so I like how I did it here much better.  On the first bar of the day, I set my variable totTrades to EasyLanguages built-in totalTrades (notice the difference in the spelling!)  The keyword TotalTrades is immediately updated when a trade is closed out.  So, I simply subtract my totTrades (the total number of trades at the beginning of the day) from totalTrades.  If totalTrades  is incremented (a trade is closed out) then the difference between the two variables is 1.  I assign the difference of these two values to longEntriesToday.  If longEntriesToday = 1, then I know I have entered long and have been stopped out.  I then say, OK let’s get back long at a better price – the midpoint.  I use the longEntriesToday variable again to determine which exit to use.  With any luck the initial momentum that got us long initially will work its way back into the market for another shot.

It’s Easy to Create  a Strategy Based Indicator

Once you develop a strategy, the indicator that plots entry and exit levels is very easy to derive.  You have already done the math – just plot the values.  Check this out.

//working with 5 minute bars here
//should work with any time frame

input:startTradeTime(0930),numOfBarsHHLL(6);
vars: startTime(0),endTime(0),
barCountToday(0),periodHigh(-99999999),periodLow(99999999);

startTime = sessionStartTime(0,1);
endTime = sessionStartTime(0,1);


if t = calcTime(startTime,barInterval) Then
begin
periodHigh = -99999999;
periodLow = 99999999;
barCountToday = 1;
end;

if barCountToday <= numOfBarsHHLL Then
Begin
periodHigh = maxList(periodHigh,h);
periodLow = minList(periodLow,l);
end;

if barCountToday < numOfBarsHHLL Then
begin
noPlot(1);
noPlot(2);
noPlot(3);
noPlot(4);
End
Else
begin
plot1(periodHigh,"top");
plot2((periodHigh+periodLow)/2,"mid");
plot3(periodLow,"bot");
plot4(periodLow - (periodHigh - periodLow),"2bot");
end;

barCountToday = barCountToday + 1;

So, that’s how you do it.  You can use this code as a foundation for any system that wants to try and reenter at a better price.  You can also use this code to develop your own time based break out strategy.  In my new book, Easing Into EasyLanguage – the Daytrade Edition I will discuss topics very similar to this post.

 

 

 

The EasyLanguage Function: A Thing of Beauty

The concept of Scope when it comes to a programming language can get very confusing very quickly. If you are new to programming and you think Scope is a mouthwash, no worries here. I will give a full explanation of Scope in terms of programming with the help of ChatGPT. Its response is italicized.

I recently chatted with ChatGPT about Scope in terms of the Python programing language.

In Python, the scope of a variable refers to the region of the program where that variable can be accessed or referenced. There are two types of scopes in Python: global scope and local scope.

Here’s an example that demonstrates the concept of scope in Python:

# global variable
x = 10

def my_func():
# local variable
y = 5
print("x inside my_func:", x) # x is accessed from the global scope
print("y inside my_func:", y) # y is accessed from the local scope

my_func()

print("x outside my_func:", x) # x is accessed from the global scope
print("y outside my_func:") # y is not defined in the global scope, so this will raise a NameError
Example of Scope as demonstrated by Chat-GPT

In this example, we define a global variable x with a value of 10.

We then define a function my_func that has a local variable y with a value of 5. Inside the function, we print the values of x and y. The variable x is accessed from the global scope, which means it can be referenced from anywhere in the program.

The variable y, on the other hand, is only accessible within the function, which is the local scope. Outside the function, we print the value of x again, which is still accessible from the global scope. However, when we try to print the value of y, we get a NameError because y is not defined in the global scope.

Okay that was pretty formal. In simpler terms, a variable declared inside a function can only be seen inside that function, unless it is declared globally. Another thing about Scope, is that the variables have amnesia. Once you step out of the function the variable forgets what it was, and you can’t refer to its prior value upon return to the function.

Unless you are an EasyLanguage function. Scope is still constrained by an EasyLanguage function, because it is a separate piece of code held within the library of Analysis Techniques. Most of the time you can’t see what’s in the function unless you open it with the ELEditor. However, the variables that are defined inside the function do not suffer from amnesia. If you need to refer to a prior value of a locally declared variable, you can. This type of function is what EasyLanguage calls a Serial function. The only downside to this function is it slows processing down quite a bit.

Okay. To make a long story short I wanted to show the magic of EasyLanguage function that I have been working with on a project. This project includes some of Ehlers’ cycle analysis functions. The one I am going to discuss today is the HighRoof function – don’t worry I am not going to go into detail of what this function does. If you want to know just GOOGLE it or ask ChatGPT. I developed a strategy that used the function on the last 25 days of closing price data. I then turned around and fed the output of the first pass of the HighRoof function right back into the HighRoof function. Something similar to embedding functions.

doubleSmooth = average(average(c,20),20);

Sort of like a double smoothed moving average. After I did this, I started thinking does the function remember the data from its respective call? The first pass used closing price data, so its variables and their history should be in terms of price data. The second pass used the cyclical movements data that was output by the initial call to the HighRoof function. Everything turned out fine, the function remembered the correct data. Or seemed like it did. This is how you learn about any programming language – pull out your SandBox and do some testing. First off, here is my conversion of Ehlers’ HighRoof function in EasyLanguage.

//Ehlers HiRoof
Inputs: dataSeries(numericseries),cutPeriod(Numeric);

Vars: a1(0), b1(0), c1(0), c2(0), c3(0), Filt(0), Filt2(0),
alpha1(0),oneMinusAlpha1(0), highPass(0),myhp(0),degrees(0);
Vars: numTimesCalled(0);

//Highpass filter cyclic components whose periods are shorter than 48 bars

numTimesCalled = numTimesCalled + 1;

print(d," numTimesCalled ",numTimesCalled," highPass[1] ",highPass[1]," highPass[2] ",highPass[2]," highPass[3] ",highPass[3]);
degrees = .707*360 / CutPeriod;

alpha1 = (Cosine(degrees) + Sine(degrees) - 1) / Cosine(degrees);

oneMinusAlpha1 = 1-alpha1;

highPass = square(oneMinusAlpha1/2)*(dataSeries-2*dataSeries[1]+dataSeries[2]) +
2*(oneMinusAlpha1)*highPass[1]-square(oneMinusAlpha1)*highPass[2];



EhlersHighRoof=highPass;
Ehlers High Roof Function

This function requires just two inputs – the data (with a history) and a simple length or cut period. The first input is of type numericSeries and the second input is of type numericSimple. You will see the following line of code

 print(d," numTimesCalled ",numTimesCalled," highPass[1] ",highPass[1]," highPass[2] ",highPass[2]," highPass[3] ",highPass[3]);

This code prints out the last three historic values of the HighPass variable for each function call. I am calling the function twice for each bar of data in the Crude Oil futures continuous contract.

1230206.00 numTimesCalled  494.00 highPass[1]   -0.78 highPass[2]   -0.51 highPass[3]   -0.60
1230206.00 numTimesCalled 494.00 highPass[1] -0.05 highPass[2] -0.02 highPass[3] -0.06
1230207.00 numTimesCalled 495.00 highPass[1] -0.38 highPass[2] -0.78 highPass[3] -0.51
1230207.00 numTimesCalled 495.00 highPass[1] 0.04 highPass[2] -0.05 highPass[3] -0.02
1230208.00 numTimesCalled 496.00 highPass[1] 0.31 highPass[2] -0.38 highPass[3] -0.78
1230208.00 numTimesCalled 496.00 highPass[1] 0.16 highPass[2] 0.04 highPass[3] -0.05
1230209.00 numTimesCalled 497.00 highPass[1] 0.49 highPass[2] 0.31 highPass[3] -0.38
1230209.00 numTimesCalled 497.00 highPass[1] 0.15 highPass[2] 0.16 highPass[3] 0.04
1230210.00 numTimesCalled 498.00 highPass[1] 0.30 highPass[2] 0.49 highPass[3] 0.31
1230210.00 numTimesCalled 498.00 highPass[1] 0.07 highPass[2] 0.15 highPass[3] 0.16
1230213.00 numTimesCalled 499.00 highPass[1] 0.52 highPass[2] 0.30 highPass[3] 0.49
1230213.00 numTimesCalled 499.00 highPass[1] 0.08 highPass[2] 0.07 highPass[3] 0.15
1230214.00 numTimesCalled 500.00 highPass[1] 0.44 highPass[2] 0.52 highPass[3] 0.30
1230214.00 numTimesCalled 500.00 highPass[1] 0.04 highPass[2] 0.08 highPass[3] 0.07
Output of calling HighRoof twice per bar

Starting at the top of the output you will see that on 1230206 the function was called twice with two different sets of data. As you can see the output of the first two lines is of a different magnitude. The first line is approximately an order or magnitude of 10 of the second line. If you go to lines 3 and 4 you will see the highPass[1] of lines 1 and 2 moves to highPass[2] and then onto highPass[3]. I think what happens internally is for every call on per bar basis, the variables for each function call are pushed into a queue in memory. The queue continues to grow for whatever length is necessary and then either maintained or truncated at some later time.

Why Is This So Cool?

In many languages the encapsulation of data with the function requires additional programming. The EasyLanguage function could be seen as an “object” like in object-oriented programming. You just don’t know you are doing it. EasyLanguage takes care of a lot of the behind-the-scenes data management. To do the same thing in Python you would need to create a class of Ehlers Roof that maintain historic data in class members and the calculations would be accomplished by a class method. In the case of calling the function twice, you would instantiate two classes from the template and each class would act independent of each other.

Here is my SandBox for Indicator


Value1 = EhlersHighRoof(close,25);
plot1(Value1,"EhlersHiRoof");
Value2 = EhlersHighRoof(value1,25);
plot2(Value2,"EhlersHiRoof2");
Sandbox Playground for Ehlers Function

 

One last nugget of information. If you are going to be working with trigonometric functions such as Cosine, Sine or Tangent, make sure your arguments are in degrees not radians. In Python, you must use radians.