Automating Trading Strategies in MQL5 (Part 52): The tCISD Model with SSMT and Quarterly Theory
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:
- Understanding the tCISD Model with SSMT and Quarterly Theory
- Implementation in MQL5
- Backtesting
- 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.

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.

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.

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.

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.

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.

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.

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).

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.

The backtest report is shown below.

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.
Warning: All rights to these materials are reserved by MetaQuotes Ltd. Copying or reprinting of these materials in whole or in part is prohibited.
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.
From Novice to Expert: Systematic Profit Conservation Using Candle Range Theory
Quick Integration of a Large Language Model into MetaTrader 5 (Part I): Building the Model
Designing a Unified Order Execution Gateway Class in MQL5
The Avellaneda-Stoikov Model: Inventory-Aware Quoting for Two-Sided Strategies
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use