Skip to content
← Indicator library
tradingviewvolatilityindicatorpinescript-v6

AlgoNexta CT3 — Candle Three Expansion & Pullback

Detects three-candle range expansions, maps pullback zones, and tracks analytical stop and R-target geometry.

Published by AlgoNexa. AlgoNexa builds and documents evidence-first trading tools so you can inspect the logic before using it.About AlgoNexaIndicator library
Published Aug 18, 2026#ct3#candlestick-pattern#volatility#pullback#price-action#indicator#pinescript-v6#tradingview

How it works

Read the logic before you use it.

1

When it signals

A Rise or Fall setup requires a confirmed directional C3 with a range at least equal to **Minimum Range Ratio** times the combined C1+C2 range and a body at least **Minimum C3 Body %** of C3's range. Defaults are `1.5` and `40.0%` respectively.

2

What confirms it

Optional controls can require C1/C2 direction, similar candle ranges, a C3 close beyond the prior structure, ATR expansion limits, wick limits, and trend alignment. **Trend mode** defaults to `None`; optional EMA alignment uses length `50`, and confirmed pivots use strength `3` by default.

3

How risk is framed

The default `C1+C2 Zone` and midpoint **Reference Entry** create a pullback reference. The suggested stop is placed beyond the full CT3 extreme plus the selected buffer, and `1R to 6R` target lines are derived from that reference risk; these drawings are not trades.

Walkthrough

Watch how it reads the chart.

Live market chart

The logic on real candles.

Backtest evidence

What the numbers actually show.

This script's logic could not be confidently reconstructed into an automated reference backtest, so no performance figures are shown rather than estimated ones.

Verify it in TradingView's Strategy Tester (or the equivalent tool for tradingview) before relying on it.

Vision Grade

What was measured, and how confident we are in it.

Vision Grade requires a reference backtest with enough reconstructed entries to measure — not available for this script yet.

Full breakdown

How the script behaves.

About AlgoNexa

Published by AlgoNexa, an evidence-first platform for building, reviewing, and documenting algorithmic trading tools. This page explains the script's actual logic and intended use; it does not promise profitability or replace verification in TradingView.

What it does

AlgoNexta CT3 identifies a three-candle expansion pattern. It compares the current candle (C3) with the combined range of the prior two candles (C1 and C2), then marks either a bearish CT3 Fall or bullish CT3 Rise when C3 expands sufficiently and closes in the expected direction.

With the default Minimum Range Ratio of 1.5, C3 must have a range at least 1.5 times the combined C1+C2 range. C3 must also meet the default Minimum C3 Body % of 40.0. The script can then draw an analytical pullback zone, a reference entry, a suggested stop, and optional R-multiple target lines. It is an indicator only: it does not place or simulate orders.

What the pattern checks

A CT3 Fall requires a bearish C3; a CT3 Rise requires a bullish C3. By default, both Enable CT3 Fall and Enable CT3 Rise are on.

Core default requirements:

  • C3 range / combined C1+C2 range is at least 1.5 (Minimum Range Ratio).
  • C3 body is at least 40.0% of its own range (Minimum C3 Body %).
  • Zero-range candles are excluded when Exclude zero-range C1/C2/C3 is enabled (default true).
  • Signals are evaluated only once the bar is confirmed (barstate.isconfirmed).

Optional filters are off by default and can make detection more selective:

  • Require expected C1/C2 direction can require the first two candles to point opposite the C3 expansion; Allow neutral/doji in C1/C2 controls whether small-bodied candles can qualify.
  • Use C1/C2 similarity limits the range difference between C1 and C2 using Maximum range difference %.
  • Require C3 close beyond C1/C2 structure and Require C3 close near extreme add close-location requirements.
  • ATR and wick filters include Use C3 ATR expansion, Use maximum C3 ATR, Limit C1+C2 combined range, Limit total C3 wick, and Limit opposite C3 wick.
  • Trend mode defaults to None. Available alignment methods include EMA, EMA plus slope, market structure, and EMA plus market structure. The default EMA length is 50; the default EMA slope lookback is 3.
  • Use confirmed HTF EMA context is off by default. When enabled, it uses the last completed candle from the selected Higher timeframe (60 by default), with lookahead disabled.

The script also assigns a configurable CT3 Score from 0 to 100. Show CT3 Score is on by default, while Filter signals by score is off. The displayed grade is a score label, not a performance rating: A+ is 90+, A is 80+, B is 70+, C is 60+, and lower scores are Weak.

Reading the chart

  • CT3 RISE labels mark bullish expansion setups. Their default color is green (CT3 Rise).
  • CT3 FALL labels mark bearish expansion setups. Their default color is red (CT3 Fall).
  • The shaded box is the potential entry zone. Zone source defaults to C1+C2 Zone; choose Full CT3 Zone or Both to change the source. Displayed portion defaults to Entire Zone and can show top, bottom, midpoint, or a custom percentage band.
  • The dashed white line is the Reference Entry, set to Midpoint by default. It can instead use the zone high, zone low, or Reference Entry custom %.
  • The orange line is the suggested stop. By default, Stop buffer mode is Fixed Price and Stop buffer value is 1.0. Tick-based and ATR-based buffers are also available.
  • Blue dotted lines are optional R targets. R targets defaults to 1R to 6R; these are chart geometry derived from the reference entry and suggested stop, not orders or guaranteed targets.
  • Show SL / TP price labels is enabled by default. The optional C3 extreme and manual context target can add further target lines when directionally valid.
  • Setup status updates on closed bars, including Waiting, Zone Touched, target reached, Invalidated, Ambiguous, and Expired. Terminal setups are faded gray.
  • Show latest-setup dashboard is off by default. When enabled, the top-right table shows the latest setup's direction, quality, score grade, range ratio, and lifecycle status.

Lifecycle and alerts

By default, a setup is invalidated on a Close beyond its suggested stop. Change Invalidation to Wick for high/low-based invalidation, or Disabled to stop invalidation tracking.

Expiration defaults to After N Bars, with Expiration bars set to 50. It can instead be set to Never or Opposite CT3. If a bar reaches both a target and the stop, Same-bar SL/target resolution defaults to Ambiguous, because OHLC data does not reveal the intrabar sequence. Conservative and Optimistic alternatives are available.

Standard TradingView alert conditions are available for new Qualified, Standard, and Strong Rise/Fall patterns; zone touches; invalidations; same-bar ambiguity; TP1 through TP6; manual context targets; and expiration. Enable dynamic alert() events is off by default; when enabled, dynamic alerts fire once per confirmed bar. Include expiration in dynamic alerts controls whether expiry is included in those dynamic messages.

Markets, timeframes, and limitations

The code does not restrict the symbol or chart timeframe, so it can be applied to any TradingView market with OHLC data. Because it evaluates three completed candles and may draw zones for up to Drawing extension bars (200 by default), use a timeframe where individual candle ranges are meaningful for your market and review the instrument's tick size before using a fixed-price stop buffer.

  • CT3 is a candle-range pattern detector, not an execution strategy, trend predictor, or complete trade plan.
  • Signals appear only after the C3 bar closes, so they cannot be known during that candle's formation.
  • Market-structure mode uses confirmed pivots. With the default Confirmed pivot strength of 3, pivots become usable only on their confirmation bar, three bars late.
  • Confirmed HTF EMA context uses prior completed higher-timeframe values with lookahead disabled; it does not use an unfinished HTF candle.
  • Stop, entry, and target drawings are analytical references. They do not account for spreads, slippage, fills, commissions, liquidity, or intrabar order sequencing.
  • Same-bar stop/target events remain inherently uncertain from OHLC data; the script explicitly marks the default case as Ambiguous.
  • This indicator is not financial advice. Use TradingView Strategy Tester or an independently specified strategy as the source of truth for historical and live execution results.

Source

pinescript-v6 · full source

algonexta-ct3-candle-three-expansion-pullback-2.pine
// ============================================================================
// © algonexta
// This script is for education and research; it does not guarantee results.
// Verify this source in TradingView before use.
// ============================================================================

//@version=6
indicator("AlgoNexta CT3 — Candle Three Expansion & Pullback", shorttitle="AN CT3", overlay=true,
     max_boxes_count=200, max_lines_count=500, max_labels_count=500, max_bars_back=1000)

// CT3 v1.0 RC1 — release candidate.
// CT3 identifies third-candle volatility expansion and maps analytical geometry.
// It does not provide investment advice, execute trades, or simulate execution.
// Zone percentages always run from price-low (0%) to price-high (100%).
// Confirmed pivots become usable only on their confirmation bar (pivotLength bars late).
// Optional HTF context uses the last completed HTF candle with lookahead disabled.

string G1="01 — CT3 Pattern", G2="02 — Trend", G3="03 — Volatility", G4="04 — Entry Zone"
string G5="05 — Stop & Risk Geometry", G6="06 — Visual Targets", G7="07 — CT3 Score"
string G8="08 — Lifecycle", G9="09 — Visual Style", G10="10 — Alerts", G11="11 — Advanced"

bool enableFall=input.bool(true,"Enable CT3 Fall",group=G1)
bool enableRise=input.bool(true,"Enable CT3 Rise",group=G1)
float minRatio=input.float(1.5,"Minimum Range Ratio",minval=0.1,step=0.1,group=G1)
float standardRatio=input.float(3.0,"Standard Range Ratio",minval=0.1,step=0.1,group=G1)
float strongBodyRatio=input.float(3.0,"Strong Body Ratio",minval=0.1,step=0.1,group=G1)
float minBodyPct=input.float(40.0,"Minimum C3 Body %",minval=0,maxval=100,group=G1)
bool requirePairDirection=input.bool(false,"Require expected C1/C2 direction",group=G1)
bool allowNeutral=input.bool(false,"Allow neutral/doji in C1/C2",group=G1)
float neutralBodyPct=input.float(5.0,"Neutral maximum Body %",minval=0,maxval=25,group=G1)
bool useSimilarity=input.bool(false,"Use C1/C2 similarity",group=G1)
float maxDifferencePct=input.float(100,"Maximum range difference %",minval=0,group=G1)
bool useCloseBeyond=input.bool(false,"Require C3 close beyond C1/C2 structure",group=G1)
float closeBeyondPct=input.float(100,"Required structure traversal %",minval=0,maxval=100,group=G1,
 tooltip="Fall: C3 close must be at/below this normalized level from C1/C2 high toward low. Rise is mirrored.")
bool useCloseExtreme=input.bool(false,"Require C3 close near extreme",group=G1)
float closeExtremePct=input.float(20,"Maximum distance from favorable extreme %",minval=0,maxval=100,group=G1)

string trendMode=input.string("None","Trend mode",options=["None","EMA","EMA + Slope","Market Structure","EMA + Market Structure"],group=G2)
int emaLength=input.int(50,"EMA length",minval=1,group=G2)
int slopeLookback=input.int(3,"EMA slope lookback",minval=1,group=G2)
int pivotLength=input.int(3,"Confirmed pivot strength",minval=1,maxval=20,group=G2)
bool useHTF=input.bool(false,"Use confirmed HTF EMA context",group=G2)
string htf=input.timeframe("60","Higher timeframe",group=G2)

int atrLength=input.int(14,"ATR length",minval=1,group=G3)
bool useC3ATR=input.bool(false,"Use C3 ATR expansion",group=G3)
float minC3ATR=input.float(0.5,"Minimum C3 ATR multiple",minval=0,step=0.1,group=G3)
bool useMaxC3ATR=input.bool(false,"Use maximum C3 ATR",group=G3)
float maxC3ATR=input.float(5.0,"Maximum C3 ATR multiple",minval=0.1,step=0.1,group=G3)
bool useCombinedATR=input.bool(false,"Limit C1+C2 combined range",group=G3)
float maxCombinedATR=input.float(2.0,"Maximum C1+C2 ATR multiple",minval=0.1,group=G3)
bool useTotalWick=input.bool(false,"Limit total C3 wick",group=G3)
float maxTotalWickPct=input.float(50,"Maximum total wick %",minval=0,maxval=100,group=G3)
bool useOppositeWick=input.bool(false,"Limit opposite C3 wick",group=G3)
float maxOppositeWickPct=input.float(25,"Maximum opposite wick %",minval=0,maxval=100,group=G3)

string zoneSource=input.string("C1+C2 Zone","Zone source",options=["C1+C2 Zone","Full CT3 Zone","Both"],group=G4)
string zonePortion=input.string("Entire Zone","Displayed portion",options=["Entire Zone","Top 25%","Bottom 25%","Midpoint Band","Custom Band"],group=G4)
float zoneStart=input.float(40,"Custom start %",minval=0,maxval=100,group=G4)
float zoneEnd=input.float(60,"Custom end %",minval=0,maxval=100,group=G4)
string entryMode=input.string("Midpoint","Reference Entry",options=["Zone High","Zone Low","Midpoint","Custom Percentage"],group=G4)
float entryPct=input.float(50,"Reference Entry custom %",minval=0,maxval=100,group=G4)

string bufferMode=input.string("Fixed Price","Stop buffer mode",options=["Fixed Price","Ticks","ATR-Based"],group=G5)
float bufferValue=input.float(1.0,"Stop buffer value",minval=0,step=0.1,group=G5)

string targetMode=input.string("1R to 6R","R targets",options=["Disabled","1R","2R","3R","1R to 3R","1R to 6R","Custom R"],group=G6)
bool showLevelLabels=input.bool(true,"Show SL / TP price labels",group=G6)
float customR=input.float(2.5,"Custom R",minval=0.1,group=G6)
bool showC3Extreme=input.bool(false,"Show C3 extreme target when directionally valid",group=G6)
bool useManualTarget=input.bool(false,"Enable Manual Context Target",group=G6)
float manualTarget=input.price(0,"Manual Target Price",group=G6)
string manualLabel=input.string("Context Target","Manual Target Label",group=G6)

bool showScore=input.bool(true,"Show CT3 Score",group=G7)
bool filterByScore=input.bool(false,"Filter signals by score",group=G7)
float minScore=input.float(0,"Minimum score",minval=0,maxval=100,group=G7)
int wRange=input.int(25,"Range expansion weight",minval=0,group=G7)
int wBodyExpansion=input.int(15,"Body expansion weight",minval=0,group=G7)
int wBodyQuality=input.int(10,"Body-to-range weight",minval=0,group=G7)
int wSimilarity=input.int(10,"Similarity weight",minval=0,group=G7)
int wTrend=input.int(15,"Trend alignment weight",minval=0,group=G7)
int wSlope=input.int(10,"EMA slope weight",minval=0,group=G7)
int wATR=input.int(5,"ATR expansion weight",minval=0,group=G7)
int wWick=input.int(10,"Wick quality weight",minval=0,group=G7)

string invalidationMode=input.string("Close","Invalidation",options=["Wick","Close","Disabled"],group=G8)
string expiryMode=input.string("After N Bars","Expiration",options=["After N Bars","Never","Opposite CT3"],group=G8)
int expiryBars=input.int(50,"Expiration bars",minval=1,maxval=1000,group=G8)
string sameBarMode=input.string("Ambiguous","Same-bar SL/target resolution",options=["Ambiguous","Conservative","Optimistic"],group=G8,
 tooltip="OHLC cannot reveal intrabar order. Ambiguous is the commercial default.")

string preset=input.string("Standard","Visual preset",options=["Minimal","Standard","Detailed"],group=G9)
bool showHistory=input.bool(true,"Show historical setups",group=G9)
bool onlyLatest=input.bool(false,"Only latest setup",group=G9)
int maxSetups=input.int(30,"Maximum stored setups",minval=1,maxval=30,group=G9)
int extensionBars=input.int(200,"Drawing extension bars",minval=1,maxval=500,group=G9)
bool showEMA=input.bool(true,"Show EMA",group=G9)
bool showDashboard=input.bool(false,"Show latest-setup dashboard",group=G9)
color fallColor=input.color(color.rgb(205,68,78),"CT3 Fall",group=G9)
color riseColor=input.color(color.rgb(35,164,123),"CT3 Rise",group=G9)
color stopColor=input.color(color.rgb(235,145,55),"Suggested stop",group=G9)
color targetColor=input.color(color.rgb(55,145,210),"Targets",group=G9)

bool dynamicAlerts=input.bool(false,"Enable dynamic alert() events",group=G10)
bool alertExpiry=input.bool(false,"Include expiration in dynamic alerts",group=G10)
bool excludeZeroRange=input.bool(true,"Exclude zero-range C1/C2/C3",group=G11,
 tooltip="Release-candidate safety control. Zero-range candles cannot form valid CT3 ratios.")

type CandleMetrics
    float candleRange
    float body
    float upper
    float lower
    float bodyPct
    bool bull
    bool bear
    bool neutral

type CT3Setup
    int direction
    int signalBar
    string quality
    float rangeRatio
    float bodyRatio
    float bodyPct
    float score
    float zoneLow
    float zoneHigh
    float entry
    float stop
    float tp1
    float tp2
    float tp3
    float tp4
    float tp5
    float tp6
    float extraTarget
    float contextTarget
    bool has1
    bool has2
    bool has3
    bool has4
    bool has5
    bool has6
    bool hasExtra
    bool hasContext
    bool touched
    bool hit1
    bool hit2
    bool hit3
    bool hit4
    bool hit5
    bool hit6
    bool hitExtra
    bool hitContext
    bool terminal
    string status
    box primaryBox
    box secondaryBox
    line entryLine
    line stopLine
    line tp1Line
    line tp2Line
    line tp3Line
    line tp4Line
    line tp5Line
    line tp6Line
    line extraLine
    line contextLine
    label signalLabel
    label detailLabel
    label stopPriceLabel
    label tp1PriceLabel
    label tp2PriceLabel
    label tp3PriceLabel
    label tp4PriceLabel
    label tp5PriceLabel
    label tp6PriceLabel

metrics(int n) =>
    float r=high[n]-low[n], b=math.abs(close[n]-open[n])
    CandleMetrics.new(r,b,high[n]-math.max(open[n],close[n]),math.min(open[n],close[n])-low[n],r>0?b/r*100:0,
      close[n]>open[n],close[n]<open[n],r==0 or (r>0 and b/r*100<=neutralBodyPct))

float ema=ta.ema(close,emaLength), atr=ta.atr(atrLength)
float ph=ta.pivothigh(high,pivotLength,pivotLength), pl=ta.pivotlow(low,pivotLength,pivotLength)
float lastPH=ta.valuewhen(not na(ph),ph,0), prevPH=ta.valuewhen(not na(ph),ph,1)
float lastPL=ta.valuewhen(not na(pl),pl,0), prevPL=ta.valuewhen(not na(pl),pl,1)
int structure=not na(prevPH) and not na(prevPL) and lastPH>prevPH and lastPL>prevPL?1:not na(prevPH) and not na(prevPL) and lastPH<prevPH and lastPL<prevPL?-1:0
float htfClose=request.security(syminfo.tickerid,htf,close[1],gaps=barmerge.gaps_off,lookahead=barmerge.lookahead_off)
float htfEMA=request.security(syminfo.tickerid,htf,ta.ema(close,emaLength)[1],gaps=barmerge.gaps_off,lookahead=barmerge.lookahead_off)
float htfEMAPast=request.security(syminfo.tickerid,htf,ta.ema(close,emaLength)[1+slopeLookback],gaps=barmerge.gaps_off,lookahead=barmerge.lookahead_off)

trendParts(int d) =>
    bool e=d==1?close>ema:close<ema
    bool s=d==1?ema>ema[slopeLookback]:ema<ema[slopeLookback]
    bool m=structure==d
    bool base=trendMode=="None"?true:trendMode=="EMA"?e:trendMode=="EMA + Slope"?e and s:trendMode=="Market Structure"?m:e and m
    bool h=not useHTF or (d==1?htfClose>htfEMA and htfEMA>htfEMAPast:htfClose<htfEMA and htfEMA<htfEMAPast)
    [base and h,e,s,m]

grade(float s)=>s>=90?"A+":s>=80?"A":s>=70?"B":s>=60?"C":"Weak"
clamp01(float x)=>math.max(0.0,math.min(1.0,x))

scoreSetup(float rr,float br,float bp,float diff,float c3atr,float wick,int d,bool trendOK,bool slopeOK) =>
    // Disabled optional components are removed and remaining weights are renormalized.
    float raw=0, total=0
    raw+=wRange*clamp01((rr-minRatio)/math.max(standardRatio-minRatio,0.01)+0.5)
    total+=wRange
    raw+=wBodyExpansion*clamp01(br/math.max(strongBodyRatio,0.01))
    total+=wBodyExpansion
    raw+=wBodyQuality*clamp01(bp/80)
    total+=wBodyQuality
    if useSimilarity
        raw+=wSimilarity*clamp01(1-diff/math.max(maxDifferencePct,1))
        total+=wSimilarity
    if trendMode!="None" or useHTF
        raw+=wTrend*(trendOK?1:0)
        total+=wTrend
    if trendMode=="EMA + Slope" or useHTF
        raw+=wSlope*(slopeOK?1:0)
        total+=wSlope
    if useC3ATR
        raw+=wATR*clamp01(c3atr/math.max(minC3ATR*2,0.01))
        total+=wATR
    if useTotalWick or useOppositeWick
        raw+=wWick*clamp01(1-wick/100)
        total+=wWick
    total>0?math.round(raw/total*100):0

detect(int d) =>
    CandleMetrics c1=metrics(2), c2=metrics(1), c3=metrics(0)
    float combined=c1.candleRange+c2.candleRange, rr=combined>0?c3.candleRange/combined:0, br=combined>0?c3.body/combined:0
    float avg=combined/2, diff=avg>0?math.abs(c1.candleRange-c2.candleRange)/avg*100:na
    float totalWick=c3.candleRange>0?(c3.upper+c3.lower)/c3.candleRange*100:100
    float opposite=c3.candleRange>0?(d==-1?c3.lower:c3.upper)/c3.candleRange*100:100
    bool expected1=d==-1?c1.bull:c1.bear, expected2=d==-1?c2.bull:c2.bear
    bool pair=not requirePairDirection or ((expected1 or (allowNeutral and c1.neutral)) and (expected2 or (allowNeutral and c2.neutral)))
    bool directionOK=d==-1?c3.bear:c3.bull
    float c12Low=math.min(low[2],low[1]), c12High=math.max(high[2],high[1]), span=c12High-c12Low
    float fallLimit=c12High-span*closeBeyondPct/100, riseLimit=c12Low+span*closeBeyondPct/100
    bool beyond=not useCloseBeyond or (span>0 and (d==-1?close<=fallLimit:close>=riseLimit))
    float extremeDist=c3.candleRange>0?(d==-1?close-low:high-close)/c3.candleRange*100:100
    float c3atr=atr>0?c3.candleRange/atr:0
    [trendOK,emaOK,slopeOK,structureOK]=trendParts(d)
    bool rangesOK=not excludeZeroRange or (c1.candleRange>0 and c2.candleRange>0 and c3.candleRange>0)
    bool valid=barstate.isconfirmed and bar_index>=2 and combined>0 and c3.candleRange>0 and rangesOK and pair and directionOK and
      rr>=minRatio and c3.bodyPct>=minBodyPct and (not useSimilarity or (not na(diff) and diff<=maxDifferencePct)) and
      (not useCombinedATR or (atr>0 and combined<=atr*maxCombinedATR)) and (not useC3ATR or c3atr>=minC3ATR) and
      (not useMaxC3ATR or c3atr<=maxC3ATR) and (not useTotalWick or totalWick<=maxTotalWickPct) and
      (not useOppositeWick or opposite<=maxOppositeWickPct) and beyond and (not useCloseExtreme or extremeDist<=closeExtremePct) and trendOK
    string q=br>=strongBodyRatio?"Strong":rr>=standardRatio?"Standard":"Qualified"
    float sc=scoreSetup(rr,br,c3.bodyPct,nz(diff,200),c3atr,totalWick,d,trendOK,slopeOK)
    [valid,q,rr,br,c3.bodyPct,sc]

zone(float lo,float hi) =>
    float a=zonePortion=="Entire Zone"?0:zonePortion=="Top 25%"?75:zonePortion=="Bottom 25%"?0:zonePortion=="Midpoint Band"?40:math.min(zoneStart,zoneEnd)
    float b=zonePortion=="Entire Zone"?100:zonePortion=="Top 25%"?100:zonePortion=="Bottom 25%"?25:zonePortion=="Midpoint Band"?60:math.max(zoneStart,zoneEnd)
    [lo+(hi-lo)*a/100,lo+(hi-lo)*b/100]

deleteGraphics(CT3Setup s) =>
    if not na(s.primaryBox)
        box.delete(s.primaryBox)
    if not na(s.secondaryBox)
        box.delete(s.secondaryBox)
    if not na(s.entryLine)
        line.delete(s.entryLine)
    if not na(s.stopLine)
        line.delete(s.stopLine)
    if not na(s.tp1Line)
        line.delete(s.tp1Line)
    if not na(s.tp2Line)
        line.delete(s.tp2Line)
    if not na(s.tp3Line)
        line.delete(s.tp3Line)
    if not na(s.tp4Line)
        line.delete(s.tp4Line)
    if not na(s.tp5Line)
        line.delete(s.tp5Line)
    if not na(s.tp6Line)
        line.delete(s.tp6Line)
    if not na(s.extraLine)
        line.delete(s.extraLine)
    if not na(s.contextLine)
        line.delete(s.contextLine)
    if not na(s.signalLabel)
        label.delete(s.signalLabel)
    if not na(s.detailLabel)
        label.delete(s.detailLabel)
    if not na(s.stopPriceLabel)
        label.delete(s.stopPriceLabel)
    if not na(s.tp1PriceLabel)
        label.delete(s.tp1PriceLabel)
    if not na(s.tp2PriceLabel)
        label.delete(s.tp2PriceLabel)
    if not na(s.tp3PriceLabel)
        label.delete(s.tp3PriceLabel)
    if not na(s.tp4PriceLabel)
        label.delete(s.tp4PriceLabel)
    if not na(s.tp5PriceLabel)
        label.delete(s.tp5PriceLabel)
    if not na(s.tp6PriceLabel)
        label.delete(s.tp6PriceLabel)

fade(CT3Setup s) =>
    color muted=color.new(color.gray,75)
    if not na(s.primaryBox)
        box.set_bgcolor(s.primaryBox,color.new(color.gray,94))
        box.set_border_color(s.primaryBox,muted)
    if not na(s.secondaryBox)
        box.set_bgcolor(s.secondaryBox,color.new(color.gray,96))
        box.set_border_color(s.secondaryBox,muted)
    if not na(s.entryLine)
        line.set_color(s.entryLine,muted)
    if not na(s.stopLine)
        line.set_color(s.stopLine,muted)
    if not na(s.tp1Line)
        line.set_color(s.tp1Line,muted)
    if not na(s.tp2Line)
        line.set_color(s.tp2Line,muted)
    if not na(s.tp3Line)
        line.set_color(s.tp3Line,muted)
    if not na(s.tp4Line)
        line.set_color(s.tp4Line,muted)
    if not na(s.tp5Line)
        line.set_color(s.tp5Line,muted)
    if not na(s.tp6Line)
        line.set_color(s.tp6Line,muted)
    if not na(s.extraLine)
        line.set_color(s.extraLine,muted)
    if not na(s.contextLine)
        line.set_color(s.contextLine,muted)
    if not na(s.stopPriceLabel)
        label.set_textcolor(s.stopPriceLabel,muted)
    if not na(s.tp1PriceLabel)
        label.set_textcolor(s.tp1PriceLabel,muted)
    if not na(s.tp2PriceLabel)
        label.set_textcolor(s.tp2PriceLabel,muted)
    if not na(s.tp3PriceLabel)
        label.set_textcolor(s.tp3PriceLabel,muted)
    if not na(s.tp4PriceLabel)
        label.set_textcolor(s.tp4PriceLabel,muted)
    if not na(s.tp5PriceLabel)
        label.set_textcolor(s.tp5PriceLabel,muted)
    if not na(s.tp6PriceLabel)
        label.set_textcolor(s.tp6PriceLabel,muted)

updateLabel(CT3Setup s) =>
    if not na(s.detailLabel)
        string dir=s.direction==1?"CT3 Rise":"CT3 Fall"
        string compact=dir+" · "+grade(s.score)+"\n"+str.tostring(s.rangeRatio,"#.##")+"x\n"+s.status
        string detailed=dir+"\n"+s.quality+" · "+grade(s.score)+"\nRange "+str.tostring(s.rangeRatio,"#.##")+"x · Body "+str.tostring(s.bodyRatio,"#.##")+"x\nBody "+str.tostring(s.bodyPct,"#.#")+"%\nEntry "+str.tostring(s.entry,format.mintick)+"\nStop "+str.tostring(s.stop,format.mintick)+"\n"+s.status
        label.set_text(s.detailLabel,preset=="Detailed"?detailed:compact)

createSetup(int d,string q,float rr,float br,float bp,float sc) =>
    float pHigh=math.max(high,math.max(high[1],high[2])), pLow=math.min(low,math.min(low[1],low[2]))
    float cHigh=math.max(high[1],high[2]), cLow=math.min(low[1],low[2])
    bool full=zoneSource=="Full CT3 Zone"
    [zLow,zHigh]=zone(full?pLow:cLow,full?pHigh:cHigh)
    float ep=entryMode=="Zone Low"?0:entryMode=="Zone High"?100:entryMode=="Midpoint"?50:entryPct
    float en=zLow+(zHigh-zLow)*ep/100
    float buf=bufferMode=="Fixed Price"?bufferValue:bufferMode=="Ticks"?bufferValue*syminfo.mintick:atr*bufferValue
    float st=d==-1?pHigh+buf:pLow-buf, risk=d==-1?st-en:en-st
    bool riskOK=risk>0
    float t1=en+d*risk, t2=en+d*risk*2, t3=en+d*risk*3
    float t4=en+d*risk*4, t5=en+d*risk*5, t6=en+d*risk*6
    bool to3=targetMode=="1R to 3R" or targetMode=="1R to 6R"
    bool to6=targetMode=="1R to 6R"
    bool h1=riskOK and (targetMode=="1R" or to3)
    bool h2=riskOK and (targetMode=="2R" or to3)
    bool h3=riskOK and (targetMode=="3R" or to3)
    bool h4=riskOK and to6
    bool h5=riskOK and to6
    bool h6=riskOK and to6
    float extra=targetMode=="Custom R" and riskOK?en+d*risk*customR:d==-1?low:high
    bool hExtra=(targetMode=="Custom R" and riskOK) or (showC3Extreme and (d==-1?extra<en:extra>en))
    bool hContext=useManualTarget and manualTarget>0 and (d==-1?manualTarget<en:manualTarget>en)
    color c=d==1?riseColor:fallColor, emphasis=q=="Strong"?0:q=="Standard"?15:35, muted=color.new(c,emphasis)
    int right=bar_index+extensionBars
    bool boxes=preset!="Minimal"
    box b1=boxes?box.new(bar_index,zHigh,right,zLow,border_color=muted,bgcolor=color.new(c,86),text="Potential Entry Zone",text_color=color.new(color.white,35),text_size=size.tiny):na
    box b2=na
    if boxes and zoneSource=="Both"
        [sLow,sHigh]=zone(full?cLow:pLow,full?cHigh:pHigh)
        b2:=box.new(bar_index,sHigh,right,sLow,border_color=color.new(c,55),bgcolor=color.new(c,94))
    line el=preset!="Minimal"?line.new(bar_index,en,right,en,color=color.new(color.white,20),style=line.style_dashed):na
    line sl=preset!="Minimal"?line.new(bar_index,st,right,st,color=stopColor,width=2):na
    line l1=h1?line.new(bar_index,t1,right,t1,color=targetColor,style=line.style_dotted):na
    line l2=h2?line.new(bar_index,t2,right,t2,color=targetColor,style=line.style_dotted):na
    line l3=h3?line.new(bar_index,t3,right,t3,color=targetColor,style=line.style_dotted):na
    line l4=h4?line.new(bar_index,t4,right,t4,color=targetColor,style=line.style_dotted):na
    line l5=h5?line.new(bar_index,t5,right,t5,color=targetColor,style=line.style_dotted):na
    line l6=h6?line.new(bar_index,t6,right,t6,color=targetColor,style=line.style_dotted):na
    line lx=hExtra?line.new(bar_index,extra,right,extra,color=targetColor,style=line.style_dotted):na
    line lc=hContext?line.new(bar_index,manualTarget,right,manualTarget,color=color.new(targetColor,10),width=2):na
    label sig=label.new(bar_index,d==-1?high:low,d==-1?"CT3 FALL":"CT3 RISE",style=d==-1?label.style_label_down:label.style_label_up,color=muted,textcolor=color.white,size=size.tiny)
    label det=preset=="Minimal"?na:label.new(bar_index,d==-1?high:low,"",style=d==-1?label.style_label_down:label.style_label_up,color=color.new(c,20),textcolor=color.white,size=size.tiny)
    label slTag=showLevelLabels?label.new(right,st,"SL  "+str.tostring(st,format.mintick),style=label.style_none,textcolor=stopColor,size=size.tiny):na
    label t1Tag=showLevelLabels and h1?label.new(right,t1,"TP1 · 1R  "+str.tostring(t1,format.mintick),style=label.style_none,textcolor=targetColor,size=size.tiny):na
    label t2Tag=showLevelLabels and h2?label.new(right,t2,"TP2 · 2R  "+str.tostring(t2,format.mintick),style=label.style_none,textcolor=targetColor,size=size.tiny):na
    label t3Tag=showLevelLabels and h3?label.new(right,t3,"TP3 · 3R  "+str.tostring(t3,format.mintick),style=label.style_none,textcolor=targetColor,size=size.tiny):na
    label t4Tag=showLevelLabels and h4?label.new(right,t4,"TP4 · 4R  "+str.tostring(t4,format.mintick),style=label.style_none,textcolor=targetColor,size=size.tiny):na
    label t5Tag=showLevelLabels and h5?label.new(right,t5,"TP5 · 5R  "+str.tostring(t5,format.mintick),style=label.style_none,textcolor=targetColor,size=size.tiny):na
    label t6Tag=showLevelLabels and h6?label.new(right,t6,"TP6 · 6R  "+str.tostring(t6,format.mintick),style=label.style_none,textcolor=targetColor,size=size.tiny):na
    CT3Setup s=CT3Setup.new(d,bar_index,q,rr,br,bp,sc,zLow,zHigh,en,st,t1,t2,t3,t4,t5,t6,extra,manualTarget,
      h1,h2,h3,h4,h5,h6,hExtra,hContext,
      false,false,false,false,false,false,false,false,false,false,"Waiting",
      b1,b2,el,sl,l1,l2,l3,l4,l5,l6,lx,lc,sig,det,slTag,t1Tag,t2Tag,t3Tag,t4Tag,t5Tag,t6Tag)
    updateLabel(s)
    s

var array<CT3Setup> setups=array.new<CT3Setup>()
[fallRaw,fallQ,fallRR,fallBR,fallBP,fallScore]=detect(-1)
[riseRaw,riseQ,riseRR,riseBR,riseBP,riseScore]=detect(1)
bool historyOK=showHistory or barstate.isrealtime
bool newFall=enableFall and fallRaw and historyOK and (not filterByScore or fallScore>=minScore)
bool newRise=enableRise and riseRaw and historyOK and (not filterByScore or riseScore>=minScore)

bool evTouch=false, evInvalid=false, evAmbiguous=false, ev1=false, ev2=false, ev3=false, ev4=false, ev5=false, ev6=false, evContext=false, evExpired=false
// Lifecycle is deliberately closed-bar only; making this optional would break reload parity.
bool lifecycleReady=barstate.isconfirmed
if lifecycleReady and array.size(setups)>0
    for i=0 to array.size(setups)-1
        CT3Setup s=array.get(setups,i)
        if not s.terminal and bar_index>s.signalBar
            bool adverse=s.direction==-1?(invalidationMode=="Wick"?high>=s.stop:invalidationMode=="Close"?close>=s.stop:false):(invalidationMode=="Wick"?low<=s.stop:invalidationMode=="Close"?close<=s.stop:false)
            bool f1=s.has1 and not s.hit1 and (s.direction==-1?low<=s.tp1:high>=s.tp1)
            bool f2=s.has2 and not s.hit2 and (s.direction==-1?low<=s.tp2:high>=s.tp2)
            bool f3=s.has3 and not s.hit3 and (s.direction==-1?low<=s.tp3:high>=s.tp3)
            bool f4=s.has4 and not s.hit4 and (s.direction==-1?low<=s.tp4:high>=s.tp4)
            bool f5=s.has5 and not s.hit5 and (s.direction==-1?low<=s.tp5:high>=s.tp5)
            bool f6=s.has6 and not s.hit6 and (s.direction==-1?low<=s.tp6:high>=s.tp6)
            bool fx=s.hasExtra and not s.hitExtra and (s.direction==-1?low<=s.extraTarget:high>=s.extraTarget)
            bool fc=s.hasContext and not s.hitContext and (s.direction==-1?low<=s.contextTarget:high>=s.contextTarget)
            bool favorable=f1 or f2 or f3 or f4 or f5 or f6 or fx or fc
            bool conflict=adverse and favorable
            if conflict and sameBarMode=="Ambiguous"
                s.terminal:=true
                s.status:="Ambiguous"
                evAmbiguous:=true
                fade(s)
            else
                bool processAdverse=adverse and (not conflict or sameBarMode=="Conservative")
                bool processFavorable=favorable and (not conflict or sameBarMode=="Optimistic")
                if processAdverse
                    s.terminal:=true
                    s.status:="Invalidated"
                    evInvalid:=true
                    fade(s)
                else if processFavorable
                    if f1
                        s.hit1:=true
                        ev1:=true
                    if f2
                        s.hit2:=true
                        ev2:=true
                    if f3
                        s.hit3:=true
                        ev3:=true
                    if f4
                        s.hit4:=true
                        ev4:=true
                    if f5
                        s.hit5:=true
                        ev5:=true
                    if f6
                        s.hit6:=true
                        ev6:=true
                    if fx
                        s.hitExtra:=true
                    if fc
                        s.hitContext:=true
                        evContext:=true
                    s.status:=fc?"Context Target Reached":f6?"TP6 Reached":f5?"TP5 Reached":f4?"TP4 Reached":f3?"TP3 Reached":f2?"TP2 Reached":f1?"TP1 Reached":"Target Reached"
                if not s.terminal and not s.touched and high>=s.zoneLow and low<=s.zoneHigh
                    s.touched:=true
                    s.status:="Zone Touched"
                    evTouch:=true
                if not s.terminal and expiryMode=="After N Bars" and bar_index-s.signalBar>=expiryBars
                    s.terminal:=true
                    s.status:="Expired"
                    evExpired:=true
                    fade(s)
            updateLabel(s)

if lifecycleReady and expiryMode=="Opposite CT3" and (newFall or newRise) and array.size(setups)>0
    int nd=newFall?-1:1
    for i=0 to array.size(setups)-1
        CT3Setup s=array.get(setups,i)
        if not s.terminal and s.direction!=nd
            s.terminal:=true
            s.status:="Expired — Opposite CT3"
            evExpired:=true
            fade(s)
            updateLabel(s)

if newFall or newRise
    if onlyLatest
        while array.size(setups)>0
            CT3Setup old=array.shift(setups)
            deleteGraphics(old)
    CT3Setup fresh=newFall?createSetup(-1,fallQ,fallRR,fallBR,fallBP,fallScore):createSetup(1,riseQ,riseRR,riseBR,riseBP,riseScore)
    array.push(setups,fresh)
    while array.size(setups)>maxSetups
        CT3Setup old=array.shift(setups)
        deleteGraphics(old)

plot(showEMA and trendMode!="None"?ema:na,"Trend EMA",color=color.new(color.rgb(236,181,47),10),linewidth=2)

bool qFall=newFall and fallQ=="Qualified", sFall=newFall and fallQ=="Standard", xFall=newFall and fallQ=="Strong"
bool qRise=newRise and riseQ=="Qualified", sRise=newRise and riseQ=="Standard", xRise=newRise and riseQ=="Strong"
alertcondition(qFall,"Qualified CT3 Fall","Qualified CT3 Fall — {{ticker}} {{interval}}")
alertcondition(sFall,"Standard CT3 Fall","Standard CT3 Fall — {{ticker}} {{interval}}")
alertcondition(xFall,"Strong CT3 Fall","Strong CT3 Fall — {{ticker}} {{interval}}")
alertcondition(qRise,"Qualified CT3 Rise","Qualified CT3 Rise — {{ticker}} {{interval}}")
alertcondition(sRise,"Standard CT3 Rise","Standard CT3 Rise — {{ticker}} {{interval}}")
alertcondition(xRise,"Strong CT3 Rise","Strong CT3 Rise — {{ticker}} {{interval}}")
alertcondition(evTouch,"Entry Zone Touched","CT3 Entry Zone Touched — {{ticker}} {{interval}}")
alertcondition(evInvalid,"Setup Invalidated","CT3 Setup Invalidated — {{ticker}} {{interval}}")
alertcondition(evAmbiguous,"Same-Bar Ambiguous","CT3 same-bar outcome is Ambiguous — {{ticker}} {{interval}}")
alertcondition(ev1,"TP1 Reached","CT3 TP1 Reached — {{ticker}} {{interval}}")
alertcondition(ev2,"TP2 Reached","CT3 TP2 Reached — {{ticker}} {{interval}}")
alertcondition(ev3,"TP3 Reached","CT3 TP3 Reached — {{ticker}} {{interval}}")
alertcondition(ev4,"TP4 Reached","CT3 TP4 Reached — {{ticker}} {{interval}}")
alertcondition(ev5,"TP5 Reached","CT3 TP5 Reached — {{ticker}} {{interval}}")
alertcondition(ev6,"TP6 Reached","CT3 TP6 Reached — {{ticker}} {{interval}}")
alertcondition(evContext,"Manual Context Target Reached","CT3 Manual Context Target Reached — {{ticker}} {{interval}}")
alertcondition(evExpired,"Setup Expired","CT3 Setup Expired — {{ticker}} {{interval}}")
if dynamicAlerts and barstate.isconfirmed and (newFall or newRise or evTouch or evInvalid or evAmbiguous or ev1 or ev2 or ev3 or ev4 or ev5 or ev6 or evContext or (alertExpiry and evExpired))
    string event=newFall or newRise?"New "+(newFall?fallQ+" CT3 Fall":riseQ+" CT3 Rise"):evAmbiguous?"Ambiguous":evInvalid?"Invalidated":evContext?manualLabel+" Reached":ev6?"TP6 Reached":ev5?"TP5 Reached":ev4?"TP4 Reached":ev3?"TP3 Reached":ev2?"TP2 Reached":ev1?"TP1 Reached":evTouch?"Zone Touched":"Expired"
    alert("AlgoNexta CT3 | "+event+" | "+syminfo.ticker+" | "+timeframe.period,alert.freq_once_per_bar_close)

var table dash=table.new(position.top_right,2,6,border_width=1)
if barstate.islast
    table.clear(dash,0,0,1,5)
    if showDashboard and array.size(setups)>0
        CT3Setup last=array.get(setups,array.size(setups)-1)
        table.cell(dash,0,0,"CT3",text_color=color.white,bgcolor=color.rgb(35,40,48))
        table.cell(dash,1,0,"v1.0 RC1",text_color=color.white,bgcolor=color.rgb(35,40,48))
        table.cell(dash,0,1,"Direction")
        table.cell(dash,1,1,last.direction==1?"Rise":"Fall")
        table.cell(dash,0,2,"Quality")
        table.cell(dash,1,2,last.quality)
        table.cell(dash,0,3,"Grade")
        table.cell(dash,1,3,grade(last.score))
        table.cell(dash,0,4,"Range")
        table.cell(dash,1,4,str.tostring(last.rangeRatio,"#.##")+"x")
        table.cell(dash,0,5,"Status")
        table.cell(dash,1,5,last.status)

FAQ

Common questions.

What does AlgoNexta CT3 detect?

It detects a third candle whose range expands relative to the combined ranges of the prior two candles. A CT3 Rise requires a bullish C3 and a CT3 Fall requires a bearish C3, subject to the enabled range, body, trend, ATR, wick, and close-location filters.

Does CT3 repaint?

The CT3 pattern is evaluated only on confirmed bars, so a signal is not produced until C3 closes. Market-structure filters use confirmed pivots and are delayed by the selected Confirmed pivot strength; optional HTF context uses completed higher-timeframe candles with lookahead disabled.

Does this script place trades or backtest results?

No. It is an overlay indicator and does not call strategy entry or exit functions. Its entry, stop, and R-target lines are analytical geometry, not orders or simulated fills.

How are the entry zone, stop, and targets calculated?

The zone defaults to the C1+C2 range, with the Reference Entry at its Midpoint. The suggested stop sits beyond the full three-candle extreme plus the selected Stop buffer value, while optional targets are calculated as 1R through 6R from the entry-to-stop distance.

Which markets and timeframes can I use with CT3?

The code does not impose a symbol or timeframe restriction, so it can run on TradingView charts with OHLC data. Select a timeframe where candle ranges are meaningful for the instrument, and consider tick size when using the default Fixed Price stop buffer.

How do alerts work, and is this financial advice?

TradingView alert conditions cover new CT3 setups, zone touches, invalidations, targets, ambiguity, and expiry; Enable dynamic alert() events optionally sends confirmed-bar dynamic messages. This is not financial advice, and TradingView Strategy Tester or an equivalent execution-aware test is the source of truth for live results.

Want a version tuned to your market?

Describe the change in plain English and AlgoNexta rebuilds it as a fresh Pine v6 script — with its own explanation, evidence, and walkthrough.

Build your own version

Educational content only — not financial advice. TradingView is the source of truth for live data, compilation, and Strategy Tester results.