preview
Automating Trading Strategies in MQL5 (Part 52): The tCISD Model with SSMT and Quarterly Theory

Automating Trading Strategies in MQL5 (Part 52): The tCISD Model with SSMT and Quarterly Theory

MetaTrader 5Trading |
246 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Introduction

Traders familiar with smart money concepts can often spot individual signals, such as liquidity sweeps, session structure, or hints of a reversal, but struggle to turn them into a repeatable, mechanical decision. The pain points are specific: no fixed time anchor for cycles, daylight saving time (DST) and broker-offset issues that break synchronization, misaligned bars across correlated symbols that create false divergences, and the absence of a deterministic trigger price to translate a divergence into an entry. The result is a strategy that still relies on feel rather than a testable rule set.

In our previous article (Part 51), we automated the Bread and Butter Judas Swing model that traded session sweeps into premium and discount. In this article, we formalize the tCISD model as a program for MetaQuotes Language 5 (MQL5) developers and systematic traders who want a concrete, auditable implementation. It combines Quarterly Theory cycles anchored to New York time, with daylight saving handled automatically, a Sequential Smart Money Technique (SSMT) divergence measured bar-for-bar against a correlated symbol, and a precise trigger, the open of the last opposing candle, that defines the tCISD level. We implement it as a program driven by a finite state machine that moves from idle to confirmation to retest, with configurable entry modes, risk or lot sizing, broker-safe stop and target handling, optional probability filters (True Open, premium and discount, two-stage SSMT), a trailing stop, visuals, and logging, so you can compile it, run it in the Strategy Tester, and iterate reproducibly. We will cover the following topics:

  1. Understanding the tCISD Model with SSMT and Quarterly Theory
  2. Implementation in MQL5
  3. Backtesting
  4. Conclusion


Understanding the tCISD Model with SSMT and Quarterly Theory

The tCISD model rests on the idea that time, not just price, decides when a market is likely to turn. Quarterly Theory divides the trading day into repeating cycles, and each cycle into four quarters, anchored to a fixed point in New York time rather than the broker's clock. The first quarter tends to accumulate; the second delivers the initial move and sets what we treat as the True Open; the third often reverses or manipulates; and the fourth distributes. Working on this grid means we stop asking only what price is doing and start asking where in the cycle it is doing it, turning a vague sense of timing into fixed windows we can test against.

Layered on that timing is the divergence that gives the model its edge. A Sequential Smart Money Technique divergence compares our traded symbol against a correlated one across the same quarter and fires when the two disagree on which extreme was taken. If our symbol sweeps its previous-quarter high but the correlated symbol fails to sweep its own, one of them is lying about the strength of that move, and that disagreement warns us the high is a liquidity grab rather than a genuine breakout, with the mirror case on the lows warning of a bullish reversal. The divergence suggests a reversal is likely, but it does not define the trigger price. That is the role of the tCISD level: the open of the last opposing candle. A break of that level marks the change in state of delivery and confirms commitment to the new direction. The swept quarter extreme becomes the stop anchor. See an illustration of the quarterly cycles below.

TCISD QUARTERLY THEORY MODEL ILLUSTRATION

In live trading, wait for a quarter to close and check whether your symbol and its correlated partner disagree on the sweep before you look for the tCISD level to trade against. Decide in advance whether you take the confirmation close, which enters sooner with less certainty, or the retest, which waits for price to return to the level for a better price but sometimes never fills. Tighten the odds by demanding the level sit on the correct side of the True Open or in the right half of the previous cycle's range. Place your stop beyond the swept extreme, so that if the divergence resolves and both markets finally sweep together, you are already out. Have a look below at what we will be building. We will use Gold (XAUUSD) and Silver (XAGUSD) as the primary and correlation symbols respectively for illustration.

tCISD MODEL WITH SSMT AND QUARTERLY THEORY ROADMAP


Implementation in MQL5

We open the implementation by laying the groundwork the rest of the program stands on: the trade library, the enumerations that expose our choices, the full input set, and the global state that carries the cycles, the divergence, and the setup between bars.

//+------------------------------------------------------------------+
//|                                                     tCISD EA.mq5 |
//|                           Copyright 2026, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Allan Munene Mutiiria."
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"

//--- Include the standard library for order execution
#include <Trade\Trade.mqh>

//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+
enum LotSizingMode
  {
   LOTS_FIXED,        // Fixed lot size
   LOTS_RISK_PERCENT  // Risk percent of balance (auto lot)
  };

enum EntryMode
  {
   ENTRY_RETEST,      // Enter on a retest of the tCISD level
   ENTRY_CONFIRM      // Enter on the confirmation close
  };

enum SetupState
  {
   STATE_IDLE,        // No active setup
   STATE_CONFIRM,     // Awaiting the confirmation close through the level
   STATE_RETEST       // Awaiting the retest of the level
  };

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input group "GENERAL"
input long            MagicNumber = 1107;                        // Magic Number
input LotSizingMode   InpLotSizingMode      = LOTS_RISK_PERCENT; // Lot sizing mode
input double          InpFixedLots          = 0.01;             // Fixed lot
input double          InpRiskPercent        = 0.5;              // Risk per trade (percent of balance)
input string          InpOrderComment       = "tCISD";          // Order comment

input group "CORRELATED SYMBOL"
input string          InpCorrelatedSymbol   = "XAGUSDm";        // Correlated symbol for the SSMT

input group "QUARTERLY THEORY CYCLE"
input int             InpCycleMinutes       = 90;               // Cycle length in minutes (quarter = cycle / 4)
input int             InpBrokerGmtOffset    = 0;                // Broker GMT offset in hours (New York DST automatic)

input group "STOP LOSS"
input int             InpStopBufferPoints   = 300;               // Buffer beyond the SSMT extreme (points)
input int             InpMinStopPoints      = 3000;             // Skip the trade if the stop is closer than this (points)

input group "TAKE PROFIT"
input double          InpRewardRiskRatio    = 2.0;              // Reward to risk ratio

input group "ENTRY"
input ENUM_TIMEFRAMES InpEntryTimeframe     = PERIOD_CURRENT;   // Entry timeframe
input EntryMode       InpEntryMode          = ENTRY_CONFIRM;     // Enter on retest or on confirmation
input int             InpMaxConfirmBars     = 24;               // Bars to wait for the close-through
input int             InpMaxRetestBars      = 24;               // Bars to wait for the retest
input int             InpMaxScanBars        = 100;              // Bars to scan back for the opposing candle

input group "PROBABILITY FILTERS"
input bool            InpUse2StageSSMT      = false;            // Require the SSMT on the cycle above too
input bool            InpUseTrueOpen        = false;             // Bearish above / bullish below the Q2 True Open
input bool            InpUsePremiumDiscount = false;             // tCISD level in the correct half of the previous cycle

input group "TRAILING STOP"
input bool            InpUseTrailingStop    = true;            // Use trailing stop
input int             InpMinProfitPoints    = 1000;             // Minimum profit to activate trailing (points)
input int             InpTrailPoints        = 300;              // Trailing distance (points)

input group "LOGGING"
input bool            InpShowLogs           = true;             // Print messages to the Journal
input string          InpLogPrefix          = "tCISD> ";        // Log prefix

input group "VISUALS"
input bool            InpDrawVisuals        = true;             // Draw structure on the chart
input bool            InpDrawQuarters       = true;             // Draw quarter dividers and labels
input int             InpMarkerSize         = 10;               // Marker size (Wingdings 3)
input color           InpBullColor          = clrDodgerBlue;    // Bullish color
input color           InpBearColor          = clrRed;           // Bearish color
input color           InpQuarterColor       = clrSlateGray;     // Quarter divider and label color
input color           InpSsmtColor          = clrMagenta;       // SSMT marker color
input color           InpTcisdColor         = clrDarkViolet;    // tCISD level color

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
CTrade   trade;                        // Trade execution object
int      symbolDigits;                 // Cached symbol digits
double   symbolPoint;                  // Cached symbol point size
string   correlatedSymbol;             // Resolved correlated symbol name
bool     correlatedReady = false;      // True once the correlated symbol is selected
int      quarterSeconds  = 1350;       // Quarter length in seconds (cycle / 4)
datetime lastBarTime     = 0;          // Last processed entry-TF bar time

//--- Cached New York DST offset (recomputed once per day)
int      newYorkOffset    = -5;        // Current NY GMT offset in hours
int      newYorkOffsetDay = -1;        // Day-of-year the offset was resolved

//--- Quarter range tracking (current and previous quarter)
datetime quarterStart         = 0;     // Server start time of the current quarter
int      quarterOfCycle       = 0;     // Index of the quarter within its cycle (0-3)
bool     havePreviousQuarter  = false; // True once a previous quarter is stored
double   primaryQuarterHigh;           // Running primary quarter high
double   primaryQuarterLow;            // Running primary quarter low
double   correlatedQuarterHigh;        // Running correlated quarter high
double   correlatedQuarterLow;         // Running correlated quarter low
double   primaryPrevQuarterHigh;       // Primary previous-quarter high
double   primaryPrevQuarterLow;        // Primary previous-quarter low
double   correlatedPrevQuarterHigh;    // Correlated previous-quarter high
double   correlatedPrevQuarterLow;     // Correlated previous-quarter low

//--- Cycle range tracking (current and previous cycle)
datetime cycleStart           = 0;     // Server start time of the current cycle
bool     havePreviousCycle    = false; // True once a previous cycle is stored
double   primaryCycleHigh;             // Running primary cycle high
double   primaryCycleLow;              // Running primary cycle low
double   correlatedCycleHigh;          // Running correlated cycle high
double   correlatedCycleLow;           // Running correlated cycle low
double   primaryPrevCycleHigh;         // Primary previous-cycle high
double   primaryPrevCycleLow;          // Primary previous-cycle low
double   correlatedPrevCycleHigh;      // Correlated previous-cycle high
double   correlatedPrevCycleLow;       // Correlated previous-cycle low
double   trueOpen             = 0.0;   // Q2 open used as the cycle True Open

//--- Setup state machine
SetupState setupState     = STATE_IDLE; // Current setup stage
int        setupDirection = 0;          // Armed direction: +1 buy, -1 sell
datetime   ssmtTime       = 0;          // Bar time of the detected SSMT
double     ssmtExtreme    = 0.0;        // Swept extreme used as the stop anchor
double     tcisdLevel     = 0.0;        // tCISD level whose break confirms entry
int        barsInState    = 0;          // Bars elapsed in the current stage

We start by including the standard "Trade.mqh" library so we can send and manage orders through a trade object later, then declare three enumerations that turn our configuration into readable dropdowns. The "LotSizingMode" enumeration switches between a fixed lot and a risk-based automatic lot; the "EntryMode" enumeration chooses whether we enter on a retest of the tCISD level or on the confirmation close through it; and the "SetupState" enumeration names the three stages our setup moves through: idle, awaiting the confirmation close, and awaiting the retest. That last enumeration is the backbone of the state machine that drives the whole entry sequence.

Next, we expose the inputs and group them to keep the settings window readable. The general group contains the magic number, sizing mode, risk, lot values, and order comment. A separate group defines the correlated symbol used for divergence. The Quarterly Theory group sets the cycle length, from which we derive the quarter and the New York daylight-saving offset. The remaining groups control the stop buffer and minimum stop, the reward-to-risk ratio, the entry rules, the optional probability filters, trailing, logging, and visuals.

Finally, we declare the global state the program keeps alive between ticks. A trade object handles execution, and cached symbol digits and point size save repeated lookups. The correlated symbol name and a ready flag record whether the divergence source is available. We hold the quarter length in seconds, a bar-time guard, and a cached New York offset resolved once per day. Two large blocks track structure in parallel: one for the quarter and one for the cycle. Each block holds the running high and low for both our symbol and the correlated one, plus the archived previous-quarter and previous-cycle values we compare against, alongside the True Open captured at the second quarter. The last block is the setup state machine itself. It holds the current stage, the armed direction, the time and extreme of the detected divergence, the tCISD level whose break confirms entry, and a counter of bars spent in the current stage. With that done, we will define some helper utilities that we'll use throughout the program. We will start with the utilities for the New York time-anchoring grid.

Anchoring Time to the New York Quarter Grid

Quarterly Theory only works if every bar is placed on the correct quarter and cycle, so before any detection logic we build the time helpers that resolve New York time across daylight saving and map a bar onto the grid.

//+------------------------------------------------------------------+
//| Detect the open of a new entry-TF bar                            |
//+------------------------------------------------------------------+
bool IsNewBar()
  {
//--- Read the current entry-TF bar time
   datetime time = iTime(_Symbol, InpEntryTimeframe, 0);
//--- Report a new bar and store its time when it changes
   if(time != lastBarTime) { lastBarTime = time; return true; }
//--- Report no new bar
   return false;
  }

//+------------------------------------------------------------------+
//| Compute UTC time of the Nth Sunday of a month at a given hour    |
//+------------------------------------------------------------------+
datetime NthSundayUtc(int year, int month, int nth, int hourUtc)
  {
//--- Build the first day of the month
   MqlDateTime start;
   start.year = year; start.mon = month; start.day = 1; start.hour = 0; start.min = 0; start.sec = 0;
   datetime firstOfMonth = StructToTime(start);
//--- Resolve the weekday of that first day
   TimeToStruct(firstOfMonth, start);
//--- Find the day-of-month of the first Sunday (0 = Sunday)
   int firstSunday = 1 + ((7 - start.day_of_week) % 7);
//--- Step forward to the requested Nth Sunday
   int day = firstSunday + (nth - 1) * 7;
//--- Build the final timestamp at the requested UTC hour
   MqlDateTime result;
   result.year = year; result.mon = month; result.day = day; result.hour = hourUtc; result.min = 0; result.sec = 0;
   return StructToTime(result);
  }

//+------------------------------------------------------------------+
//| Resolve the New York GMT offset for a given UTC moment           |
//+------------------------------------------------------------------+
int NewYorkGmtOffset(datetime utc)
  {
//--- Break the UTC moment into calendar fields
   MqlDateTime parts; TimeToStruct(utc, parts);
//--- Bound US DST: 2nd Sunday of March to 1st Sunday of November
   datetime daylightStart = NthSundayUtc(parts.year, 3, 2, 7);
   datetime daylightEnd   = NthSundayUtc(parts.year, 11, 1, 6);
//--- Return EDT inside the DST window
   if(utc >= daylightStart && utc < daylightEnd) return -4;
//--- Return EST outside the DST window
   return -5;
  }

//+------------------------------------------------------------------+
//| Resolve the New York offset once per day and cache it            |
//+------------------------------------------------------------------+
int NewYorkOffsetCached(datetime utc)
  {
//--- Break UTC into fields to detect a day change
   MqlDateTime parts; TimeToStruct(utc, parts);
//--- Recompute the offset only when the day changes
   if(parts.day_of_year != newYorkOffsetDay)
     {
      //--- Cache the day and its resolved offset
      newYorkOffsetDay = parts.day_of_year;
      newYorkOffset    = NewYorkGmtOffset(utc);
     }
//--- Return the cached offset
   return newYorkOffset;
  }

//+------------------------------------------------------------------+
//| Map a server bar time to its quarter and cycle boundaries        |
//+------------------------------------------------------------------+
void QuarterOf(datetime barServer, datetime &outQuarterStart, int &outQuarterOfCycle, datetime &outCycleStart)
  {
//--- Convert server time to UTC then to New York time
   datetime utc     = barServer - InpBrokerGmtOffset * 3600;
   int      offset  = NewYorkOffsetCached(utc);
   datetime newYork = utc + offset * 3600;
//--- Break New York time into fields
   MqlDateTime parts; TimeToStruct(newYork, parts);
//--- Seconds elapsed since New York midnight
   int secondsOfDay      = parts.hour * 3600 + parts.min * 60 + parts.sec;
//--- Seconds since the 18:00 NY daily anchor
   int secondsSinceAnchor = ((secondsOfDay - 18 * 3600) % 86400 + 86400) % 86400;
//--- Index of the current quarter from the anchor
   int quarterIndex      = secondsSinceAnchor / quarterSeconds;
//--- Position of the quarter within its 4-quarter cycle
   outQuarterOfCycle     = quarterIndex % 4;
//--- Full cycle length in seconds
   int cycleLength       = quarterSeconds * 4;
//--- Seconds into the current quarter and cycle
   int intoQuarter       = secondsSinceAnchor - quarterIndex * quarterSeconds;
   int intoCycle         = secondsSinceAnchor % cycleLength;
//--- Quarter and cycle start in New York time
   datetime quarterStartNewYork = newYork - intoQuarter;
   datetime cycleStartNewYork   = newYork - intoCycle;
//--- Convert the boundaries back to server time
   outQuarterStart = quarterStartNewYork - offset * 3600 + InpBrokerGmtOffset * 3600;
   outCycleStart   = cycleStartNewYork   - offset * 3600 + InpBrokerGmtOffset * 3600;
  }

We start with the "IsNewBar" function, which reads the current bar time on the entry timeframe with the iTime function and reports a new bar only when that time changes, gating the heavy per-bar work so it runs once per bar rather than on every tick. Handling New York time correctly then takes two steps, because the quarter grid is anchored to New York and daylight saving shifts that clock twice a year. The "NthSundayUtc" function computes the exact moment of the Nth Sunday of a month at a chosen hour, which we need because daylight saving time in the United States begins and ends on Sundays, and the "NewYorkGmtOffset" function uses it to bound the daylight saving window from the second Sunday of March to the first Sunday of November, returning an offset of minus four inside that window and minus five outside it. To keep this cheap, the "NewYorkOffsetCached" function resolves that offset only when the day of the year changes rather than on every call.

The heart of this group is the "QuarterOf" function, which maps a server bar time onto its quarter and cycle. It first shifts server time to UTC using the broker offset and then into New York time using the cached offset. From there it measures the seconds elapsed since New York midnight and, crucially, re-bases them against an 18:00 New York anchor, which is the point Quarterly Theory treats as the start of the daily cycle structure. Dividing those anchored seconds by the quarter length gives a running quarter index, and taking that index modulo four gives the position of the quarter within its cycle, from the first quarter through the fourth. Subtracting how far we are into the current quarter and cycle yields their start times in New York, which the function finally converts back to server time so the rest of the program can compare them directly against bar times. This mapping lets later stages track not only price action, but also the exact quarter and cycle in which it occurred. To read the correlated symbol prices, we define the following function.

Reading the Correlated Symbol

The divergence is measured against a second market, so we need a reliable way to line up the correlated symbol's price with our own bar. We define the "CorrelatedHighLow" function to fetch that symbol's high and low at a given bar time.

//+------------------------------------------------------------------+
//| Read the correlated symbol high and low at a bar time            |
//+------------------------------------------------------------------+
bool CorrelatedHighLow(datetime barTime, double &high, double &low)
  {
//--- Fail when the correlated symbol is unavailable
   if(!correlatedReady) return false;
//--- Map the bar time to a shift on the correlated series
   int shift = iBarShift(correlatedSymbol, InpEntryTimeframe, barTime, false);
   if(shift < 0) return false;
//--- Read the high and low at that shift
   high = iHigh(correlatedSymbol, InpEntryTimeframe, shift);
   low  = iLow(correlatedSymbol, InpEntryTimeframe, shift);
//--- Report success only on valid prices
   return (high > 0 && low > 0);
  }

Here, we define the "CorrelatedHighLow" function to read the correlated symbol's high and low at the moment matching one of our bars. First, we bail out when the correlated symbol was never made ready, so we never act on missing data. Then we translate our bar time into the right bar on the other symbol's series with the iBarShift function, since the two symbols do not always share identical bars, and we read the high and low at that shift. We hand both values back through the output references and report success only when the prices are valid, which is the guard the divergence logic leans on before it ever compares the two markets. Getting this alignment right matters, because comparing the wrong bars across symbols would manufacture divergences that were never really there. Next, we define helpers for sizing trading volume as below.

Sizing the Position by Risk

Before the program can place an order, we need to translate a risk percentage and a stop distance into a valid lot size. We define two functions for this: one that does the risk math and one that resolves the final lot by the chosen sizing mode.

//+------------------------------------------------------------------+
//| Convert risk percent and stop distance into a lot size           |
//+------------------------------------------------------------------+
double LotsByRisk(double entry, double stop)
  {
//--- Derive the money to risk from the account balance
   double riskMoney  = AccountInfoDouble(ACCOUNT_BALANCE) * InpRiskPercent / 100.0;
//--- Measure the stop distance in points
   double stopPoints = MathAbs(entry - stop) / symbolPoint;
//--- Abort on a zero stop distance
   if(stopPoints <= 0) return 0;
//--- Read the tick value and tick size for the symbol
   double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
   double tickSize  = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
//--- Abort on invalid tick metrics
   if(tickValue <= 0 || tickSize <= 0) return 0;
//--- Convert tick value into money per point
   double valuePerPoint = tickValue / tickSize * symbolPoint;
//--- Abort on an invalid per-point value
   if(valuePerPoint <= 0) return 0;
//--- Size the position so the stop loss equals the risk money
   double lots = riskMoney / (stopPoints * valuePerPoint);
//--- Read the broker volume constraints
   double volumeMin  = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
   double volumeMax  = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
   double volumeStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
//--- Snap the lot size down to the volume step
   if(volumeStep > 0) lots = MathFloor(lots / volumeStep) * volumeStep;
//--- Clamp within limits and normalize to two decimals
   return NormalizeDouble(MathMax(volumeMin, MathMin(volumeMax, lots)), 2);
  }

//+------------------------------------------------------------------+
//| Resolve the lot size for a trade by the selected mode            |
//+------------------------------------------------------------------+
double ResolveLots(double entry, double stop)
  {
//--- Pick fixed lots or risk-based lots by the sizing mode
   double lots = (InpLotSizingMode == LOTS_FIXED) ? InpFixedLots : LotsByRisk(entry, stop);
//--- Read the broker volume constraints
   double volumeMin  = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
   double volumeMax  = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
   double volumeStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
//--- Snap the lot size down to the volume step
   if(volumeStep > 0) lots = MathFloor(lots / volumeStep) * volumeStep;
//--- Clamp within limits and normalize to two decimals
   return NormalizeDouble(MathMax(volumeMin, MathMin(volumeMax, lots)), 2);
  }

First, we define the "LotsByRisk" function to size a position so the loss at the stop equals a fixed slice of the account. We derive the money to risk from the account balance through the AccountInfoDouble function, measure the stop distance in points, and read the symbol's tick value and tick size with the SymbolInfoDouble function to work out the money moved per point. Dividing the risk money by the stop distance and that per-point value gives us the raw lots, which we then snap down to the broker's volume step and clamp between the minimum and maximum before normalizing to two decimals. The guards along the way matter here, since a zero stop distance or an invalid tick metric would otherwise produce a meaningless or dangerous size, so we return zero and let the caller abort cleanly.

We then define the "ResolveLots" function as the single entry point the rest of the program calls. We pick either the fixed lot or the risk-based lot according to the sizing mode, then apply the same step-and-clamp treatment so that whatever we ultimately send is always a broker-legal volume. Routing every trade through this one function means the sizing rules live in a single place, and neither the confirmation entry nor the retest entry has to repeat them. For visualization, we use the following logic.

Building the Drawing Toolkit

With the timing and sizing in place, we build the drawing helpers that put the whole model on the chart: the level lines, the text labels, the divergence markers, the quarter dividers, and the trade levels that mark an actual entry.

//+------------------------------------------------------------------+
//| Draw or update a horizontal trend-line level                     |
//+------------------------------------------------------------------+
void DrawLevel(string name, datetime time1, datetime time2, double price, color clr, ENUM_LINE_STYLE style, int width)
  {
//--- Create the object on first use, otherwise move both anchors
   if(ObjectFind(0, name) < 0)
      ObjectCreate(0, name, OBJ_TREND, 0, time1, price, time2, price);
   else
     {
      //--- Move the left anchor
      ObjectMove(0, name, 0, time1, price);
      //--- Move the right anchor
      ObjectMove(0, name, 1, time2, price);
     }
//--- Apply the line color, style and width
   ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
   ObjectSetInteger(0, name, OBJPROP_STYLE, style);
   ObjectSetInteger(0, name, OBJPROP_WIDTH, width);
//--- Keep the line as a segment, not a ray
   ObjectSetInteger(0, name, OBJPROP_RAY_RIGHT, false);
//--- Make the object non-interactive and hidden from the list
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
  }

//+------------------------------------------------------------------+
//| Draw or update a chart text label                                |
//+------------------------------------------------------------------+
void DrawLabel(string name, datetime time, double price, string text, color clr, ENUM_ANCHOR_POINT anchor)
  {
//--- Create and style the label on first use
   if(ObjectFind(0, name) < 0)
     {
      //--- Create the text object at the anchor point
      ObjectCreate(0, name, OBJ_TEXT, 0, time, price);
      //--- Set the font family and size
      ObjectSetString(0, name, OBJPROP_FONT, "Arial Bold");
      ObjectSetInteger(0, name, OBJPROP_FONTSIZE, 9);
      //--- Make the label non-interactive and hidden from the list
      ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
      ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
     }
//--- Refresh the label text, color and anchor
   ObjectSetString(0, name, OBJPROP_TEXT, text);
   ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
   ObjectSetInteger(0, name, OBJPROP_ANCHOR, anchor);
//--- Reposition the label
   ObjectMove(0, name, 0, time, price);
  }

//+------------------------------------------------------------------+
//| Draw a small up or down triangle marker                          |
//+------------------------------------------------------------------+
void DrawMarker(string name, datetime time, double price, bool up, color clr, int anchor)
  {
//--- Create the marker on first use, otherwise reposition it
   if(ObjectFind(0, name) < 0)
      ObjectCreate(0, name, OBJ_TEXT, 0, time, price);
   else
      ObjectMove(0, name, 0, time, price);
//--- Use the Wingdings 3 font for triangle glyphs
   ObjectSetString(0, name, OBJPROP_FONT, "Wingdings 3");
//--- Scale the glyph by the marker size input
   ObjectSetInteger(0, name, OBJPROP_FONTSIZE, InpMarkerSize);
//--- Choose an up or down triangle glyph
   ObjectSetString(0, name, OBJPROP_TEXT, up ? "p" : "q");
//--- Apply the color and anchor
   ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
   ObjectSetInteger(0, name, OBJPROP_ANCHOR, anchor);
//--- Make the object non-interactive and hidden from the list
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
  }

//+------------------------------------------------------------------+
//| Draw or update a dotted vertical line                            |
//+------------------------------------------------------------------+
void DrawVerticalLine(string name, datetime time, color clr)
  {
//--- Create the line on first use, otherwise reposition it
   if(ObjectFind(0, name) < 0)
      ObjectCreate(0, name, OBJ_VLINE, 0, time, 0);
   else
      ObjectMove(0, name, 0, time, 0);
//--- Apply a dotted style in the given color
   ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
   ObjectSetInteger(0, name, OBJPROP_STYLE, STYLE_DOT);
   ObjectSetInteger(0, name, OBJPROP_WIDTH, 1);
//--- Send the line to the background
   ObjectSetInteger(0, name, OBJPROP_BACK, true);
//--- Make the object non-interactive and hidden from the list
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
  }

//+------------------------------------------------------------------+
//| Draw the entry, stop and target levels with an entry arrow       |
//+------------------------------------------------------------------+
void DrawTradeLevels(bool isBull, datetime time, double entry, double stop, double takeProfit)
  {
//--- Skip when visuals are disabled
   if(!VisualsAllowed()) return;
//--- Build a per-entry id from the entry time
   string id = "TC_Ent_" + IntegerToString((int)time);
//--- Span the level lines a fixed number of bars to the right
   datetime endTime = time + (datetime)(PeriodSeconds(InpEntryTimeframe) * 30);
//--- Draw the entry line
   DrawLevel(id + "_e",  time, endTime, entry,      clrDodgerBlue, STYLE_SOLID, 2);
//--- Draw the stop-loss line
   DrawLevel(id + "_sl", time, endTime, stop,       C'220,60,60',  STYLE_DASH,  1);
//--- Draw the take-profit line
   DrawLevel(id + "_tp", time, endTime, takeProfit, C'0,200,80',   STYLE_DASH,  1);
//--- Read the trigger bar high and low for arrow placement
   double barHigh = iHigh(_Symbol, InpEntryTimeframe, 1);
   double barLow  = iLow(_Symbol, InpEntryTimeframe, 1);
//--- Draw the direction arrow at the trigger bar extreme
   DrawMarker(id + "_a", time, isBull ? barLow : barHigh, isBull, isBull ? InpBullColor : InpBearColor,
              isBull ? ANCHOR_UPPER : ANCHOR_LOWER);
  }

First, we define the "DrawLevel" function to draw or refresh a horizontal line between two times at a fixed price. On first use, we create it with the ObjectCreate function as a trend line pinned to the same price at both ends, and on later calls we simply move both anchors with the ObjectMove function so we update the existing line rather than pile up duplicates. After positioning, we apply the color, style, and width, keep the line as a segment rather than a ray, and make it non-interactive and hidden from the object list. This reuse-or-create pattern is what lets us redraw a level every bar without leaking objects onto the chart, and every other line in the program routes through it.

We then define the "DrawLabel" function for the text that names each level, styling it once on first creation and refreshing its text, color, and anchor on every call. Alongside it, we define the "DrawMarker" function to place the small triangle glyphs from the Wingdings 3 font, choosing an up or down triangle by direction and scaling it with the marker size input, which is how we flag a swept extreme. For the icons to use, you can switch to any that interest you. See a table below for the font characters you can use directly.

SYMBOL FONTS

The "DrawVerticalLine" function completes the structural set, dropping a dotted vertical line into the background to divide one quarter from the next.

Finally, we define the "DrawTradeLevels" function to mark an actual trade once it opens. We lay down the entry, stop-loss, and take-profit lines spanning a fixed number of bars to the right, sizing that span from the timeframe with the PeriodSeconds function, then place a direction arrow at the trigger bar extreme read through the iHigh and iLow functions. Together, these helpers turn the abstract pieces of the model — the quarters, the divergence, the level, and the trade — into something we can read at a glance on the chart. Now, we can use these helpers to compute logic to aid in finding the tCISD levels and define the cycles.

Tracking the Quarter and Cycle Ranges

Now we reach the bookkeeping that feeds the whole model: the running highs and lows of each quarter and cycle, for both our symbol and the correlated one, along with the tCISD level finder and the setup reset. We define three functions here, with the range tracker at the center.

//+------------------------------------------------------------------+
//| Find the opposing candle open that defines the tCISD level       |
//+------------------------------------------------------------------+
double FindTcisdLevel(bool bearish)
  {
//--- Bound the scan to available history
   int limit = MathMin(InpMaxScanBars, iBars(_Symbol, InpEntryTimeframe) - 1);
//--- Walk back searching for the first opposing candle
   for(int shift = 1; shift <= limit; shift++)
     {
      //--- Read the candle open and close
      double openPrice  = iOpen(_Symbol, InpEntryTimeframe, shift);
      double closePrice = iClose(_Symbol, InpEntryTimeframe, shift);
      //--- Bearish setup: return the open of the last up candle
      if(bearish && closePrice > openPrice) return openPrice;
      //--- Bullish setup: return the open of the last down candle
      if(!bearish && closePrice < openPrice) return openPrice;
     }
//--- Report none found
   return 0;
  }

//+------------------------------------------------------------------+
//| Reset the setup state machine to idle                            |
//+------------------------------------------------------------------+
void ResetSetup(string reason)
  {
//--- Log the reset with its direction and reason when active
   if(setupState != STATE_IDLE)
      Log((setupDirection > 0 ? "Bullish" : "Bearish") + " tCISD setup reset: " + reason + ".");
//--- Clear the state, direction and bar counter
   setupState = STATE_IDLE;
   setupDirection = 0;
   barsInState = 0;
  }

//+------------------------------------------------------------------+
//| Track the quarterly and cyclic high/low ranges on each bar       |
//+------------------------------------------------------------------+
void UpdateCycles()
  {
//--- Read the just-closed primary bar time and prices
   datetime barTime = iTime(_Symbol, InpEntryTimeframe, 1);
   double primaryHigh = iHigh(_Symbol, InpEntryTimeframe, 1);
   double primaryLow  = iLow(_Symbol, InpEntryTimeframe, 1);
   double primaryOpen = iOpen(_Symbol, InpEntryTimeframe, 1);
//--- Read the matching correlated bar high and low
   double correlatedHigh = 0, correlatedLow = 0;
   bool haveCorrelated = CorrelatedHighLow(barTime, correlatedHigh, correlatedLow);
//--- Resolve which quarter and cycle this bar belongs to
   datetime newQuarterStart, newCycleStart; int newQuarterOfCycle;
   QuarterOf(barTime, newQuarterStart, newQuarterOfCycle, newCycleStart);
//--- Handle a change of cycle
   if(newCycleStart != cycleStart)
     {
      //--- Archive the finished cycle range as the previous cycle
      if(cycleStart != 0)
        {
         //--- Save the primary previous-cycle high and low
         primaryPrevCycleHigh = primaryCycleHigh; primaryPrevCycleLow = primaryCycleLow;
         //--- Save the correlated previous-cycle high and low
         correlatedPrevCycleHigh = correlatedCycleHigh; correlatedPrevCycleLow = correlatedCycleLow;
         //--- Mark a previous cycle as available
         havePreviousCycle = true;
        }
      //--- Start a new cycle seeded with this bar
      cycleStart = newCycleStart;
      primaryCycleHigh = primaryHigh; primaryCycleLow = primaryLow;
      correlatedCycleHigh = (haveCorrelated ? correlatedHigh : 0); correlatedCycleLow = (haveCorrelated ? correlatedLow : 0);
      //--- Clear the True Open for the fresh cycle
      trueOpen = 0.0;
     }
   else
     {
      //--- Extend the running cycle range with this bar
      if(primaryHigh > primaryCycleHigh) primaryCycleHigh = primaryHigh;
      if(primaryLow < primaryCycleLow) primaryCycleLow = primaryLow;
      //--- Extend the correlated cycle range when data exists
      if(haveCorrelated)
        {
         //--- Push the correlated cycle high up when exceeded
         if(correlatedCycleHigh == 0 || correlatedHigh > correlatedCycleHigh) correlatedCycleHigh = correlatedHigh;
         //--- Push the correlated cycle low down when exceeded
         if(correlatedCycleLow == 0 || correlatedLow < correlatedCycleLow) correlatedCycleLow = correlatedLow;
        }
     }
//--- Handle a change of quarter
   if(newQuarterStart != quarterStart)
     {
      //--- Archive the finished quarter range as the previous quarter
      if(quarterStart != 0)
        {
         //--- Save the primary previous-quarter high and low
         primaryPrevQuarterHigh = primaryQuarterHigh; primaryPrevQuarterLow = primaryQuarterLow;
         //--- Save the correlated previous-quarter high and low
         correlatedPrevQuarterHigh = correlatedQuarterHigh; correlatedPrevQuarterLow = correlatedQuarterLow;
         //--- Mark a previous quarter as available
         havePreviousQuarter = true;
        }
      //--- Start a new quarter seeded with this bar
      quarterStart = newQuarterStart;
      quarterOfCycle = newQuarterOfCycle;
      primaryQuarterHigh = primaryHigh; primaryQuarterLow = primaryLow;
      correlatedQuarterHigh = (haveCorrelated ? correlatedHigh : 0); correlatedQuarterLow = (haveCorrelated ? correlatedLow : 0);
      //--- Capture the Q2 open as the cycle True Open
      if(newQuarterOfCycle == 1) trueOpen = primaryOpen;
      //--- Draw the quarter divider, label and True Open line
      if(VisualsAllowed() && InpDrawQuarters)
        {
         //--- Draw the quarter divider and its label
         string quarterId = IntegerToString((int)newQuarterStart);
         DrawVerticalLine("TC_QDiv_" + quarterId, newQuarterStart, InpQuarterColor);
         DrawLabel("TC_QLbl_" + quarterId, newQuarterStart, primaryHigh, " Q" + IntegerToString(newQuarterOfCycle + 1), InpQuarterColor, ANCHOR_LEFT_LOWER);
         //--- Draw the True Open line once it is set
         if(trueOpen > 0)
           {
            //--- Draw and label the True Open across the cycle
            string cycleId = IntegerToString((int)cycleStart);
            DrawLevel("TC_TO_" + cycleId, cycleStart, barTime, trueOpen, InpQuarterColor, STYLE_DOT, 1);
            DrawLabel("TC_TOt_" + cycleId, barTime, trueOpen, " True Open", InpQuarterColor, ANCHOR_LEFT);
           }
        }
     }
   else
     {
      //--- Extend the running quarter range with this bar
      if(primaryHigh > primaryQuarterHigh) primaryQuarterHigh = primaryHigh;
      if(primaryLow < primaryQuarterLow) primaryQuarterLow = primaryLow;
      //--- Extend the correlated quarter range when data exists
      if(haveCorrelated)
        {
         //--- Push the correlated quarter high up when exceeded
         if(correlatedQuarterHigh == 0 || correlatedHigh > correlatedQuarterHigh) correlatedQuarterHigh = correlatedHigh;
         //--- Push the correlated quarter low down when exceeded
         if(correlatedQuarterLow == 0 || correlatedLow < correlatedQuarterLow) correlatedQuarterLow = correlatedLow;
        }
     }
  }

We define the "FindTcisdLevel" function to locate the level whose break marks the change in state of delivery. We walk back from the most recent closed bar, bounded by the scan limit, and return the open of the first candle that pushed against the expected reversal: the last up candle for a bearish setup or the last down candle for a bullish one. That open is the price the market must close back through, and returning it early on the first match gives us the nearest such candle rather than a stale one deep in history. Alongside it, we define the "ResetSetup" function to clear the state machine back to idle, logging the direction and reason whenever an active setup is abandoned so the Journal always explains why a setup went away.

At the center sits the "UpdateCycles" function, where we keep the quarter and cycle ranges current on every bar. We read the just-closed bar on our symbol and pull the matching bar on the correlated symbol through the "CorrelatedHighLow" function, then resolve which quarter and cycle the bar belongs to with the "QuarterOf" function we built earlier. When the cycle changes, we archive the finished cycle's high and low as the previous cycle, then seed a fresh cycle from the current bar and clear the True Open; otherwise we simply extend the running cycle range with the new bar. We handle the quarter the same way in parallel, archiving the old quarter before starting the new one, and this archive-then-reset rhythm is exactly what later lets the divergence compare a completed quarter against the one before it.

Two details inside the quarter handling carry real weight. When a new quarter opens as the second quarter of its cycle, we capture that quarter's open as the True Open, the reference the optional bias filter measures against later. And when visuals are on, we draw the quarter divider, its label, and the True Open line as each quarter forms, so the time grid the whole model rests on is visible on the chart rather than hidden in the code. Throughout, we track the correlated ranges in lockstep with our own, since the divergence check is meaningless unless both symbols are measured over the very same quarter. We will wire this up now to detect and draw the cycles to see our extent in the event handlers.

Wiring the Event Handlers

With the building blocks ready, we place them inside the event handlers the terminal calls for us. We add our startup work to the OnInit event handler, our cleanup to the OnDeinit event handler, and the first slice of our per-bar sequence to the OnTick event handler.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Cache the symbol digits and point size
   symbolDigits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
   symbolPoint  = _Point;
//--- Configure the trade object magic number and slippage
   trade.SetExpertMagicNumber(MagicNumber);
   trade.SetDeviationInPoints(20);
//--- Derive the quarter length from the cycle length
   quarterSeconds = MathMax(60, (InpCycleMinutes * 60) / 4);
//--- Select the correlated symbol for the SSMT
   correlatedSymbol = InpCorrelatedSymbol;
   correlatedReady = SymbolSelect(correlatedSymbol, true);
//--- Warn when the correlated symbol is unavailable
   if(!correlatedReady)
      Log("WARNING: correlated symbol '" + correlatedSymbol + "' could not be selected - SSMT disabled until it is available.");
//--- Reset the setup state
   setupState = STATE_IDLE; setupDirection = 0;
//--- Reset the quarter tracking state
   quarterStart = 0; havePreviousQuarter = false;
//--- Reset the cycle tracking state
   cycleStart = 0; havePreviousCycle = false; trueOpen = 0.0;
//--- Force a DST offset recompute on first use
   newYorkOffsetDay = -1;
//--- Seed the bar-time guard
   lastBarTime = iTime(_Symbol, InpEntryTimeframe, 0);
//--- Log a ready banner with the key settings
   Log("tCISD EA ready on " + _Symbol + " vs " + correlatedSymbol +
       " | cycle " + IntegerToString(InpCycleMinutes) + "m | Magic " + IntegerToString(MagicNumber));
//--- Report successful initialization
   return INIT_SUCCEEDED;
  }

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Delete our chart objects on a real removal or chart close
   if(reason == REASON_REMOVE || reason == REASON_CHARTCLOSE || reason == REASON_CLOSE)
      ObjectsDeleteAll(0, "TC_");
//--- Clear any chart comment
   Comment("");
  }

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Run per-bar logic only on a new entry-TF bar
   if(IsNewBar())
     {
      //--- Update the quarter and cycle ranges
      UpdateCycles();
      //--- Flush chart updates when visuals are shown
      if(VisualsAllowed()) ChartRedraw(0);
     }
  }

We begin with the OnInit event handler, where we prepare everything the program needs before the first tick. We cache the symbol digits and point size to avoid repeated lookups, configure the trade object with our magic number and a small deviation allowance, and derive the quarter length from the cycle length so a quarter is always a quarter of a cycle. We then select the correlated symbol with the SymbolSelect function and record whether it came back ready, warning the Journal when it did not, since without it the divergence cannot be measured. We clear the setup state machine, the quarter tracking, and the cycle tracking back to a known starting point, force a daylight saving recompute on first use, seed the bar-time guard, print a ready banner, and return INIT_SUCCEEDED to confirm the program is good to run.

We add the cleanup to the OnDeinit event handler, which the terminal calls when the program is removed, or the chart closes. We delete only our own chart objects by their shared name prefix, and only on a genuine removal or chart close rather than a routine recompile, so a parameter change does not wipe the visuals unnecessarily. Clearing the chart comment leaves the chart clean behind us.

Finally, we add the per-bar sequence to the OnTick event handler, gated by the "IsNewBar" function so the logic runs once per bar rather than on every tick. On each fresh bar, we update the quarter and cycle ranges through the "UpdateCycles" function, then flush the chart with the ChartRedraw function when visuals are on, so the newly formed quarters and levels appear on the chart the moment they are set. After compiling, we get the following result.

TCISD LEVELS DRAWING

With the levels done, we can move on and find new SSMT divergences, map and arm them.

Detecting the Divergence and Arming the Setup

This is the heart of the model, where the correlated divergence is recognized and a setup is armed against the tCISD level. We define the probability filters first, then the detection that ties them together.

//+------------------------------------------------------------------+
//| Filter by the Q2 True Open bias                                  |
//+------------------------------------------------------------------+
bool PassTrueOpen(int direction, double level)
  {
//--- Pass when the filter is disabled
   if(!InpUseTrueOpen) return true;
//--- Pass when no True Open is set yet
   if(trueOpen <= 0) return true;
//--- Require the level on the correct side of the True Open
   return (direction < 0) ? (level > trueOpen) : (level < trueOpen);
  }

//+------------------------------------------------------------------+
//| Filter by premium/discount half of the previous cycle            |
//+------------------------------------------------------------------+
bool PassPremiumDiscount(int direction, double level)
  {
//--- Pass when the filter is disabled
   if(!InpUsePremiumDiscount) return true;
//--- Pass when no valid previous cycle range exists
   if(!havePreviousCycle || primaryPrevCycleHigh <= primaryPrevCycleLow) return true;
//--- Compute the previous-cycle midpoint
   double midpoint = (primaryPrevCycleHigh + primaryPrevCycleLow) / 2.0;
//--- Require the level in the premium (sell) or discount (buy) half
   return (direction < 0) ? (level > midpoint) : (level < midpoint);
  }

//+------------------------------------------------------------------+
//| Filter by an SSMT on the cycle above the quarter                 |
//+------------------------------------------------------------------+
bool PassTwoStageSSMT(int direction)
  {
//--- Pass when the filter is disabled
   if(!InpUse2StageSSMT) return true;
//--- Fail when the cycle data needed is unavailable
   if(!havePreviousCycle || correlatedCycleHigh == 0 || correlatedPrevCycleHigh == 0) return false;
//--- Bearish: require a divergence on the cycle highs
   if(direction < 0)
      return ((primaryCycleHigh > primaryPrevCycleHigh) != (correlatedCycleHigh > correlatedPrevCycleHigh));
//--- Bullish: require a divergence on the cycle lows
   return ((primaryCycleLow < primaryPrevCycleLow) != (correlatedCycleLow < correlatedPrevCycleLow));
  }

//+------------------------------------------------------------------+
//| Combine all probability filters for a direction and level        |
//+------------------------------------------------------------------+
bool PassFilters(int direction, double level)
  {
//--- Pass only when every enabled filter passes
   return PassTwoStageSSMT(direction) && PassTrueOpen(direction, level) && PassPremiumDiscount(direction, level);
  }

//+------------------------------------------------------------------+
//| Detect an SSMT divergence and arm the setup                      |
//+------------------------------------------------------------------+
void DetectSSMT()
  {
//--- Only look for a new SSMT while idle
   if(setupState != STATE_IDLE) return;
//--- Require a previous quarter and a ready correlated symbol
   if(!havePreviousQuarter || !correlatedReady) return;
//--- Require valid correlated quarter data
   if(correlatedQuarterHigh == 0 || correlatedPrevQuarterHigh == 0) return;
//--- Flag which side each symbol swept versus its previous quarter
   bool primarySweptHigh   = (primaryQuarterHigh > primaryPrevQuarterHigh);
   bool correlatedSweptHigh = (correlatedQuarterHigh > correlatedPrevQuarterHigh);
   bool primarySweptLow    = (primaryQuarterLow < primaryPrevQuarterLow);
   bool correlatedSweptLow  = (correlatedQuarterLow < correlatedPrevQuarterLow);
//--- Anchor markers to the just-closed bar
   datetime barTime = iTime(_Symbol, InpEntryTimeframe, 1);
//--- Bearish SSMT: only one symbol swept its high
   if(primarySweptHigh != correlatedSweptHigh)
     {
      //--- Locate the bearish tCISD level from an up candle
      double level = FindTcisdLevel(true);
      //--- Arm the bearish setup when the level passes the filters
      if(level > 0 && PassFilters(-1, level))
        {
         //--- Latch the armed bearish setup awaiting a close below
         setupDirection = -1; ssmtTime = barTime; ssmtExtreme = primaryQuarterHigh; tcisdLevel = level;
         setupState = STATE_CONFIRM; barsInState = 0;
         //--- Log the bearish SSMT
         Log("Bearish SSMT (highs) | tCISD level " + DoubleToString(level, symbolDigits) + " | waiting for close below");
         //--- Mark and label the swept high
         if(VisualsAllowed())
           {
            //--- Draw the SSMT marker and label at the swept high
            string id = IntegerToString((int)barTime);
            DrawMarker("TC_SSMT_" + id, barTime, ssmtExtreme, false, InpSsmtColor, ANCHOR_LOWER);
            DrawLabel("TC_SSMTt_" + id, barTime, ssmtExtreme, " SSMT", InpSsmtColor, ANCHOR_LOWER);
           }
        }
      //--- Stop after handling the high-side case
      return;
     }
//--- Bullish SSMT: only one symbol swept its low
   if(primarySweptLow != correlatedSweptLow)
     {
      //--- Locate the bullish tCISD level from a down candle
      double level = FindTcisdLevel(false);
      //--- Arm the bullish setup when the level passes the filters
      if(level > 0 && PassFilters(1, level))
        {
         //--- Latch the armed bullish setup awaiting a close above
         setupDirection = 1; ssmtTime = barTime; ssmtExtreme = primaryQuarterLow; tcisdLevel = level;
         setupState = STATE_CONFIRM; barsInState = 0;
         //--- Log the bullish SSMT
         Log("Bullish SSMT (lows) | tCISD level " + DoubleToString(level, symbolDigits) + " | waiting for close above");
         //--- Mark and label the swept low
         if(VisualsAllowed())
           {
            //--- Draw the SSMT marker and label at the swept low
            string id = IntegerToString((int)barTime);
            DrawMarker("TC_SSMT_" + id, barTime, ssmtExtreme, true, InpSsmtColor, ANCHOR_UPPER);
            DrawLabel("TC_SSMTt_" + id, barTime, ssmtExtreme, " SSMT", InpSsmtColor, ANCHOR_UPPER);
           }
        }
     }
  }

We define three optional filters that tighten the odds before we commit to a setup. In the "PassTrueOpen" function, we require the level to sit on the correct side of the second-quarter True Open, above it for a sell and below it for a buy, so we only fade in agreement with the cycle's own bias. In the "PassPremiumDiscount" function, we compute the midpoint of the previous cycle's range and demand the level fall in the premium half for a sell or the discount half for a buy, keeping us selling expensive and buying cheap. In the "PassTwoStageSSMT" function, we ask for a matching divergence one level up, on the cycle rather than the quarter, so a higher-timeframe disagreement backs the one we found. We then gather all three behind the "PassFilters" function, which passes only when every enabled filter agrees, and each filter returns true when it is switched off so the model runs unfiltered by default.

We define the "DetectSSMT" function to find the divergence and arm the setup, and we let it run only while the state machine is idle, so a live setup is never disturbed. We first flag which extreme each symbol swept against its own previous quarter, comparing our symbol's quarter high and low to its prior values and doing the same for the correlated symbol. The divergence is the disagreement between those flags: when only one symbol swept its high, we have a bearish signal, and when only one swept its low, a bullish one. This exclusive-or test is the whole point of the technique, since it fires precisely when the two markets tell different stories about the same extreme.

On a bearish divergence, we locate the tCISD level from the last up candle through the "FindTcisdLevel" function, and if it passes the filters, we latch a bearish setup, storing the swept quarter high as the stop anchor and the level as the trigger, moving the state machine to await a confirmation close below. The bullish case mirrors it exactly, anchoring on the swept quarter low and awaiting a close above. Either way, we log the armed divergence and, when visuals are on, mark and label the swept extreme so the origin of the setup is visible on the chart. At this stage, nothing has been traded; we have only recognized the disagreement and armed the level that a later close or retest must interact with before entry. When we call the function in the tick event handler, we get the following outcome.

SSMT ARMED SETUP

We can see the setup is armed successfully. What remains now is turning the armed setup into a live position and progressing the setup.

Opening the Trade and Progressing the Setup

With a setup armed against the tCISD level, we now turn it into a live position. We define two functions here: one that sizes, protects, and sends the order, and one that walks the state machine from the divergence through to the entry.

//+------------------------------------------------------------------+
//| Size, build SL and TP, and open the trade                        |
//+------------------------------------------------------------------+
void OpenTrade(bool isBull)
  {
//--- Compute the stop buffer in price
   double buffer = InpStopBufferPoints * symbolPoint;
//--- Enter at the market on the correct side
   double entry = isBull ? NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK), symbolDigits)
                         : NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID), symbolDigits);
//--- Place the stop beyond the swept SSMT extreme
   double stop = isBull ? ssmtExtreme - buffer : ssmtExtreme + buffer;
//--- Reject the trade when the stop is closer than the minimum
   if(MathAbs(entry - stop) / symbolPoint < InpMinStopPoints) { ResetSetup("stop below minimum"); return; }
//--- Read the broker minimum stop distance (stops level or spread)
   long   stopsLevel = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
   long   spread     = SymbolInfoInteger(_Symbol, SYMBOL_SPREAD);
   double minPoints  = (double)MathMax(stopsLevel, spread);
   if(minPoints < 1) minPoints = 1;
//--- Convert the minimum distance to price
   double minDistance = minPoints * symbolPoint;
//--- Push the stop out to the broker minimum when too tight
   if(isBull) { if(entry - stop < minDistance) stop = entry - minDistance; }
   else       { if(stop - entry < minDistance) stop = entry + minDistance; }
//--- Normalize the stop and measure the risk distance
   stop = NormalizeDouble(stop, symbolDigits);
   double riskDistance = MathAbs(entry - stop);
//--- Abort on an invalid risk distance
   if(riskDistance <= 0) { ResetSetup("invalid risk distance"); return; }
//--- Set the take profit at the reward-to-risk multiple of the stop
   double takeProfit = isBull ? entry + InpRewardRiskRatio * riskDistance
                              : entry - InpRewardRiskRatio * riskDistance;
//--- Push the target out to the broker minimum when too tight
   if(isBull) { if(takeProfit - entry < minDistance) takeProfit = entry + minDistance; }
   else       { if(entry - takeProfit < minDistance) takeProfit = entry - minDistance; }
//--- Normalize the target
   takeProfit = NormalizeDouble(takeProfit, symbolDigits);
//--- Resolve the lot size for this trade
   double lots = ResolveLots(entry, stop);
//--- Abort on a lot sizing error
   if(lots <= 0) { ResetSetup("lot calc error"); return; }
//--- Send the market order on the correct side
   bool ok = isBull ? trade.Buy(lots, _Symbol, entry, stop, takeProfit, InpOrderComment)
                    : trade.Sell(lots, _Symbol, entry, stop, takeProfit, InpOrderComment);
//--- Annotate and log a successful fill
   if(ok)
     {
      //--- Draw the entry, stop and target levels
      datetime now = iTime(_Symbol, InpEntryTimeframe, 0);
      DrawTradeLevels(isBull, now, entry, stop, takeProfit);
      //--- Log the fill details
      Log((isBull ? "BUY" : "SELL") + " filled @ " + DoubleToString(entry, symbolDigits) +
          "  SL=" + DoubleToString(stop, symbolDigits) + "  TP=" + DoubleToString(takeProfit, symbolDigits) +
          "  lots=" + DoubleToString(lots, 2));
     }
   else
      //--- Log the failure reason
      Log("Open failed: " + trade.ResultRetcodeDescription());
//--- Reset the setup after the attempt
   ResetSetup("filled");
  }

//+------------------------------------------------------------------+
//| Advance the setup through confirmation and retest to entry       |
//+------------------------------------------------------------------+
void ProgressSetup()
  {
//--- Do nothing while idle
   if(setupState == STATE_IDLE) return;
//--- Count another bar in the current state
   barsInState++;
//--- Read the just-closed bar close, high and low
   double closePrice = iClose(_Symbol, InpEntryTimeframe, 1);
   double highPrice  = iHigh(_Symbol, InpEntryTimeframe, 1);
   double lowPrice   = iLow(_Symbol, InpEntryTimeframe, 1);
//--- Confirmation stage: wait for the close through the level
   if(setupState == STATE_CONFIRM)
     {
      //--- Reset if both symbols resolved the bearish divergence
      if(setupDirection < 0 && primaryQuarterHigh > primaryPrevQuarterHigh && correlatedQuarterHigh > correlatedPrevQuarterHigh) { ResetSetup("SSMT resolved (both swept highs)"); return; }
      //--- Reset if both symbols resolved the bullish divergence
      if(setupDirection > 0 && primaryQuarterLow < primaryPrevQuarterLow && correlatedQuarterLow < correlatedPrevQuarterLow) { ResetSetup("SSMT resolved (both swept lows)"); return; }
      //--- Reset if the close-through never arrived in time
      if(barsInState > InpMaxConfirmBars) { ResetSetup("no close-through in time"); return; }
      //--- Test for a confirming close through the tCISD level
      bool confirmed = (setupDirection < 0) ? (closePrice < tcisdLevel) : (closePrice > tcisdLevel);
      //--- Handle a confirmed close
      if(confirmed)
        {
         //--- Anchor the confirmed level line to the current bar
         datetime now = iTime(_Symbol, InpEntryTimeframe, 0);
         //--- Draw the confirmed tCISD level and label
         if(VisualsAllowed())
           {
            //--- Draw the tCISD level line and its label
            string id = IntegerToString((int)ssmtTime);
            DrawLevel("TC_Lvl_" + id, ssmtTime, now, tcisdLevel, InpTcisdColor, STYLE_DASHDOT, 1);
            DrawLabel("TC_Lvlt_" + id, now, tcisdLevel, (setupDirection < 0 ? " -tCISD" : " +tCISD"), InpTcisdColor, ANCHOR_LEFT);
           }
         //--- Log the confirmation
         Log((setupDirection < 0 ? "Bearish" : "Bullish") + " tCISD confirmed at " + DoubleToString(tcisdLevel, symbolDigits));
         //--- Enter immediately in confirm mode, else await the retest
         if(InpEntryMode == ENTRY_CONFIRM) { OpenTrade(setupDirection > 0); return; }
         setupState = STATE_RETEST; barsInState = 0;
        }
      //--- Stop after the confirmation stage
      return;
     }
//--- Retest stage: wait for price to return to the level
   if(setupState == STATE_RETEST)
     {
      //--- Reset if the SSMT high is violated before the retest
      if(setupDirection < 0 && highPrice > ssmtExtreme) { ResetSetup("SSMT high violated before retest"); return; }
      //--- Reset if the SSMT low is violated before the retest
      if(setupDirection > 0 && lowPrice < ssmtExtreme) { ResetSetup("SSMT low violated before retest"); return; }
      //--- Reset if the retest never arrived in time
      if(barsInState > InpMaxRetestBars) { ResetSetup("no retest in time"); return; }
      //--- Enter short on a retest up into the level
      if(setupDirection < 0 && highPrice >= tcisdLevel) OpenTrade(false);
      //--- Enter long on a retest down into the level
      else if(setupDirection > 0 && lowPrice <= tcisdLevel) OpenTrade(true);
     }
  }

Here, we define the "OpenTrade" function to execute a decision the state machine has already reached. We enter the market on the correct side and place the stop beyond the swept extreme by our buffer, then run two guards on that stop. First, we reject the trade outright when the risk is closer than our minimum stop, since too tight a stop makes the setup not worth taking. Then we read the broker's own minimum distance from the stop level or spread with the SymbolInfoInteger function and push the stop and target out to it when either sits too close, so the order is always broker-legal. From the entry-to-stop distance we take as the risk, we set the take-profit at our reward-to-risk multiple, resolve the lot through the "ResolveLots" function, and send the order through the Buy or Sell method of the trade object. On a fill, we draw the trade levels and log the details, and whatever the outcome, we reset the setup afterward, since it has had its one attempt.

We define the "ProgressSetup" function to advance an armed setup one bar at a time, and it is here that the two-stage state machine earns its place. In the confirmation stage, we watch for the market to close through the tCISD level: a close below for a bearish setup or above for a bullish one, but we abandon the setup first if the divergence resolves, meaning both symbols have now swept the same extreme and the disagreement we traded on is gone, or if too many bars pass without a close-through. On a confirmed close, we draw and label the level, then either enter immediately in confirm mode or hand the setup to the retest stage. In the retest stage, we wait for price to return to the level, entering short on a move back up into it or long on a move back down, while resetting if the swept extreme is violated before the retest arrives or the retest never comes in time. This split is what lets the same signal serve two temperaments: the confirm mode that takes the earlier, less certain entry, and the retest mode that holds out for price to come back to the level. When we call the function, we get the following outcome.

PROGRESSIVE SETUP

We can see that the setups are confirmed and traded. What remains is adding trailing stops to trail positions that advance in our favor, and we use the following logic to achieve that.

//+------------------------------------------------------------------+
//| Trail the stop on this EA's open positions                       |
//+------------------------------------------------------------------+
void ManageTrailing()
  {
//--- Do nothing when trailing is disabled
   if(!InpUseTrailingStop) return;
//--- Walk every open position from last to first
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      //--- Select the position by its ticket
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0 || !PositionSelectByTicket(ticket)) continue;
      //--- Skip positions from another EA
      if(PositionGetInteger(POSITION_MAGIC) != MagicNumber) continue;
      //--- Skip positions on another symbol
      if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
      //--- Read the position side, entry, stop and target
      bool   isBull  = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY);
      double entry   = PositionGetDouble(POSITION_PRICE_OPEN);
      double curStop = PositionGetDouble(POSITION_SL);
      double curTP   = PositionGetDouble(POSITION_TP);
      //--- Read the current bid and ask
      double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
      double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
      //--- Measure open profit in points
      double profitPoints = isBull ? (bid - entry) / symbolPoint : (entry - ask) / symbolPoint;
      //--- Trail only past the activation threshold
      if(profitPoints >= InpMinProfitPoints + InpTrailPoints)
        {
         //--- Compute the trailed stop behind price
         double newStop = isBull ? bid - InpTrailPoints * symbolPoint : ask + InpTrailPoints * symbolPoint;
         newStop = NormalizeDouble(newStop, symbolDigits);
         //--- Move the stop only when it improves protection
         bool improves = isBull ? (newStop > curStop) : (curStop == 0 || newStop < curStop);
         if(improves) trade.PositionModify(ticket, newStop, curTP);
        }
     }
  }

We define the "ManageTrailing" function to advance the stop on the positions we own, and we skip the whole routine when trailing is switched off. We walk every open position and keep only the ones that match our magic number and symbol, so we never touch a trade another program placed. For each of ours, we read its side, entry, and current stop, then measure the open profit in points from the current bid or ask. Only once that profit clears the activation threshold, the minimum profit plus the trail distance, do we compute a new stop trailing the set distance behind price. We apply it through the PositionModify method of the trade object only when it actually improves protection, tightening in our favor and never loosening, which is what lets the stop ratchet forward through a run without ever giving ground.

Now we complete the OnTick event handler by placing trailing calls into the per-bar sequence alongside the existing calls.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Run per-bar logic only on a new entry-TF bar
   if(IsNewBar())
     {
      //--- Update the quarter and cycle ranges
      UpdateCycles();
      //--- Look for a new SSMT divergence
      DetectSSMT();
      //--- Advance any armed setup toward entry
      ProgressSetup();
      //--- Flush chart updates when visuals are shown
      if(VisualsAllowed()) ChartRedraw(0);
     }
//--- Trail open positions every tick
   ManageTrailing();
  }

Upon compilation, we get the following outcome.

ACTIVE TRAILING STOP

From the visualization, we can see that the positions that advance in our favor are trailed. What remains is backtesting the program, and that is handled in the next section.


Backtesting

We compile the program and run it in the MetaTrader 5 strategy tester in visual mode, which lets us watch each quarter form and each divergence arm bar by bar. The result is captured below as a Graphics Interchange Format (GIF).

TCISD BACKTSET

During testing, the program divided each session into quarters and cycles anchored to New York time, drawing the dividers and the second-quarter True Open as they were set. It tracked the quarter high and low for both our symbol and the correlated one, and armed a setup only when the two disagreed on which extreme was swept, marking the swept extreme with its divergence label. Each armed setup then located the tCISD level from the last opposing candle and waited, entering only when a later bar closed back through that level in confirm mode, or when price returned to the level in retest mode. On the trades that ran in our favor, the trailing stop advanced behind price once profit cleared its activation threshold.

The backtest graph is shown below.

GRAPH

The backtest report is shown below.

REPORT


Conclusion

In conclusion, we converted the conceptual tCISD idea into a concrete, testable MQL5 program that addresses the practical gaps identified at the outset. Concretely, the program anchors cycles and quarters to New York time with automatic daylight saving resolution and maps server bars into that grid, avoiding time-anchor drift; aligns bars across a correlated symbol by bar-time matching to prevent spurious intermarket divergences; and tracks quarter and cycle highs and lows for both symbols, detects the SSMT divergence on an exclusive sweep disagreement, and marks the swept extreme.

The program then locates the tCISD trigger as the open of the last opposing candle and drives a clear state machine that moves from idle to confirmation to retest, turning a divergence into an actionable entry. It supports both confirmation and retest entries, fixed or risk-percent lot sizing, a stop beyond the swept extreme, take-profit by reward-to-risk, broker minimum-distance compliance, optional probability filters (True Open, premium and discount, two-stage SSMT), a trailing stop, chart visuals, and logging.

The result is a working program you can compile, run in the Strategy Tester, and tune with input parameters. It turns a previously felt process into a repeatable pipeline: detect, validate, trigger, execute, and manage.

Disclaimer: This article is for educational purposes only. Trading carries significant financial risks, and past performance during backtesting does not guarantee future results. Thorough backtesting and careful risk management are essential before deploying this program in live markets.


      Attached files |
      tCISD_EA.mq5 (46.96 KB)
      From Novice to Expert: Systematic Profit Conservation Using Candle Range Theory From Novice to Expert: Systematic Profit Conservation Using Candle Range Theory
      A hybrid exit engine for MQL5 replaces static TPs with CRT-derived structural levels. The CRT_ProfitConserve class secures a partial at the first level and then trails the remaining position by structural anchors rather than fixed pips. The article walks through the class API, essential methods, and example usage in EAs, providing a clear path to embed CRT-based exits into existing strategies.
      Quick Integration of a Large Language Model into MetaTrader 5 (Part I): Building the Model Quick Integration of a Large Language Model into MetaTrader 5 (Part I): Building the Model
      The article explores the revolutionary integration of large language models (LLMs) with the MetaTrader 5 trading platform, where AI does not simply predict prices but makes autonomous trading decisions by analyzing market context much like an experienced trader. The author highlights a fundamental difference between LLMs and classical machine learning models such as CatBoost — the ability to engage in metacognition and self-reflection, which allows the system to learn from its own mistakes and improve its strategy.
      Designing a Unified Order Execution Gateway Class in MQL5 Designing a Unified Order Execution Gateway Class in MQL5
      This class provides one point of contact for trade operations in MQL5. It rounds and clamps lot sizes, validates SL/TP against the broker's minimum distance, resolves a compatible filling policy, and applies bounded retries for transient retcodes. Calls return a structured CGatewayResult instead of raw retcodes, simplifying error handling and maintenance across strategies.
      The Avellaneda-Stoikov Model: Inventory-Aware Quoting for Two-Sided Strategies The Avellaneda-Stoikov Model: Inventory-Aware Quoting for Two-Sided Strategies
      This article builds the Avellaneda–Stoikov formulas in MQL5, feeds them with rolling estimates of mid-price volatility and a proxy for order-flow intensity, and plots the reservation price with bid and ask in real time. A bar-by-bar simulation contrasts adaptive and fixed quoting under the same fill rules. The result is a tested class, an indicator, and a backtest to improve inventory control in two‑sided strategies.