Strasmore Research
Deep Dives · Matt ConnorBy Matt Connor ·

What Is Chanlun in Technical Analysis?

Chanlun is a rule-based chart method: merged bars, fractals, strokes, segments, and the pivot. Each step defined in plain English, with the data to match.

Chanlun (缠论) is a rule-based school of technical analysis, written up in a long series of Chinese blog posts in the second half of the 2000s and reimplemented in open-source code many times since. Rather than naming shapes by eye, it rebuilds the chart in fixed stages: overlapping bars get merged, three-bar turns become fractals, fractals join into strokes, strokes compose into segments, and overlapping segments define a pivot. Every stage carries a written construction rule, which is the reason two people working from the same bars are meant to end up with the same drawing.

Rule one: containment, and why bars get merged

Two adjacent bars sit in a containment relationship (包含) when one bar's high-to-low range falls entirely inside the other's. The inside bar sets no new extreme in either direction, so the pair on its own is ambiguous. The rule folds it into one synthetic bar, keeping the higher high and higher low when the local direction is up, the lower low and lower high when it is down. The merge repeats until no containment survives, and every later stage reads the merged bars, never the raw ones. Containment is common enough to matter.

QueryAdjacent daily bars in a containment relationship, calendar 2025
The exact SQL behind every number
WITH bars AS
(
    SELECT
        ticker,
        date,
        toFloat64(max(high)) AS bar_high,
        toFloat64(min(low))  AS bar_low
    FROM global_markets.stocks_daily_aggs
    WHERE ticker IN ('AAPL', 'MSFT', 'NVDA', 'SPY', 'KO', 'JNJ')
      AND date >= '2025-01-02'
      AND date <= '2025-12-31'
    GROUP BY ticker, date
),
paired AS
(
    SELECT
        ticker,
        bar_high,
        bar_low,
        lagInFrame(bar_high) OVER (PARTITION BY ticker ORDER BY date ASC ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS prev_high,
        lagInFrame(bar_low)  OVER (PARTITION BY ticker ORDER BY date ASC ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS prev_low
    FROM bars
)
SELECT
    ticker AS symbol,
    count()                                                        AS pair_count,
    countIf((bar_high >= prev_high AND bar_low <= prev_low)
         OR (bar_high <= prev_high AND bar_low >= prev_low))       AS contained_count,
    round(100 * countIf((bar_high >= prev_high AND bar_low <= prev_low)
                     OR (bar_high <= prev_high AND bar_low >= prev_low)) / count(), 1) AS contained_pct
FROM paired
WHERE prev_high > 0
GROUP BY ticker
ORDER BY contained_pct DESC
Run this yourself

Over calendar 2025 the containment share ran from 26.5% on NVDA down to 20.9% on SPY, out of about 249 adjacent bar pairs per name. Any method that counts higher highs straight off raw bars is counting those pairs too, and the bar range at issue here is the same quantity average true range measures for volatility work.

Fractals, strokes, and segments

A fractal (分型) is the smallest turn the method recognises: three merged bars where the middle bar holds the highest high and the highest low, giving a top, or the lowest low and the lowest high, giving a bottom. It labels a local extreme rather than forecasting one, tightly enough that code finds every instance.

A stroke (笔) runs from a top fractal to the next bottom fractal, or the reverse, with a length condition: the two fractals cannot share bars, and at least one independent bar has to sit between them. That condition stops each small wiggle from qualifying.

A segment (线段) is the next rung, built out of strokes instead of bars. It takes at least three strokes travelling the same way and ends once a counter-run eats back far enough to break the sequence. The same logic repeats one rung higher, which is what makes Chanlun a hierarchy rather than a single picture. Cut the same tape into bigger bars and there are far fewer turns.

QueryThree-bar turns by bar size, SPY, September 2 to 12, 2025
The exact SQL behind every number
WITH session_minutes AS
(
    SELECT
        toUnixTimestamp(window_start) AS ts,
        toFloat64(high)               AS h,
        toFloat64(low)                AS l
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE ticker = 'SPY'
      AND window_start >= '2025-09-02 00:00:00'
      AND window_start <  '2025-09-13 00:00:00'
      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
           + toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
           + toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
),
level_bars AS
(
    SELECT
        lvl,
        intDiv(ts, lvl * 60) AS slot,
        max(h)               AS bar_high,
        min(l)               AS bar_low
    FROM
    (
        SELECT ts, h, l, arrayJoin([1, 5, 15, 30, 60]) AS lvl
        FROM session_minutes
    )
    GROUP BY lvl, slot
),
neighbours AS
(
    SELECT
        lvl,
        bar_high,
        bar_low,
        lagInFrame(bar_high)  OVER (PARTITION BY lvl ORDER BY slot ASC ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS prev_high,
        lagInFrame(bar_low)   OVER (PARTITION BY lvl ORDER BY slot ASC ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS prev_low,
        leadInFrame(bar_high) OVER (PARTITION BY lvl ORDER BY slot ASC ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING) AS next_high,
        leadInFrame(bar_low)  OVER (PARTITION BY lvl ORDER BY slot ASC ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING) AS next_low
    FROM level_bars
),
flagged AS
(
    SELECT
        lvl,
        (bar_high > prev_high AND bar_high > next_high AND bar_low > prev_low AND bar_low > next_low) AS is_top,
        (bar_low < prev_low AND bar_low < next_low AND bar_high < prev_high AND bar_high < next_high) AS is_bottom
    FROM neighbours
    WHERE prev_high > 0 AND next_high > 0
)
SELECT
    concat(toString(lvl), ' min')                                    AS bucket,
    count()                                                          AS bar_count,
    countIf(is_top)                                                  AS top_turns,
    countIf(is_bottom)                                               AS bottom_turns,
    round(100 * (countIf(is_top) + countIf(is_bottom)) / count(), 1) AS turn_pct
FROM flagged
GROUP BY lvl
ORDER BY lvl ASC
Run this yourself

Two September 2025 weeks of SPY minute bars, cut five ways. At 1 min bars the tape carries 3508 bars, 19.9% of them three-bar turns. At 60 min bars it carries 61 bars, at 16.4%. The turn rate barely moves while the count collapses. A Chanlun "level" is a rung on this ladder, each with its own strokes and pivots, and it has nothing to do with a support price.

The pivot (中枢) is what everything is anchored to

A pivot is the price band that three consecutive same-level moves all pass through. Intersect their ranges and the pivot runs from the highest of the three lows to the lowest of the three highs. When that intersection comes out empty there is no pivot, and price is travelling at that level rather than consolidating.

This is the piece worth borrowing even if you never draw a stroke: a pivot is computed from bars that have already printed, so its edges exist before any trade does, and the price that would invalidate a read is on the chart in advance. Anchored VWAP reaches for the same discipline from another direction, fixing a reference at a chosen event rather than deriving it from swings.

The intersection arithmetic shows cleanly one rung lower, on bars.

QueryThree-bar overlap versus total span, AAPL, June to August 2025
The exact SQL behind every number
WITH daily AS
(
    SELECT
        date,
        toFloat64(max(high))  AS bar_high,
        toFloat64(min(low))   AS bar_low,
        toFloat64(any(close)) AS bar_close
    FROM global_markets.stocks_daily_aggs
    WHERE ticker = 'AAPL'
      AND date >= '2025-05-19'
      AND date <= '2025-08-29'
    GROUP BY date
),
rolling AS
(
    SELECT
        date,
        bar_close,
        min(bar_high) OVER (ORDER BY date ASC ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS zone_high,
        max(bar_low)  OVER (ORDER BY date ASC ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS zone_low,
        max(bar_high) OVER (ORDER BY date ASC ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS swing_high,
        min(bar_low)  OVER (ORDER BY date ASC ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS swing_low
    FROM daily
)
SELECT
    toString(date)                                                                              AS date,
    concat(substring(monthName(toDate(date)), 1, 3), ' ', toString(toDayOfMonth(toDate(date)))) AS session_label,
    round(100 * (zone_high - zone_low) / bar_close, 2)                                          AS overlap_pct,
    round(100 * (swing_high - swing_low) / bar_close, 2)                                        AS span_pct
FROM rolling
WHERE date >= '2025-06-02'
ORDER BY date ASC
Run this yourself

Each point takes three consecutive AAPL daily bars and intersects them. The overlap line is the shared band as a percentage of the close, the span line the full high-to-low reach of the same three bars. Above zero the bars hold a price in common, a pivot in miniature; below zero they share nothing, and price is in a leg at that scale. In the last window plotted, ending Aug 29, the shared band measured -0.2% of price against a span of 2.22%. Across the 63 sessions charted the line crosses zero repeatedly, the ordinary alternation of consolidation and travel at one level.

Divergence (背驰) compares the second push with the first

Divergence is where Chanlun stops being arithmetic. The setup is two moves in the same direction separated by a pivot, with the second reaching a further extreme on less force. Force is usually measured as MACD area between the two ends, sometimes as slope or volume. Construction of the bars, strokes and pivots is procedural; the strength comparison is a convention chosen by the implementer, and implementations disagree here more than anywhere else, much as RSI differs between platforms over smoothing.

A stripped-down version of that comparison, on daily bars:

QueryNew 20-session closing highs and the run-up into each, NVDA, first half of 2025
The exact SQL behind every number
WITH daily AS
(
    SELECT
        date,
        toFloat64(any(close)) AS bar_close,
        toFloat64(max(high))  AS bar_high,
        toFloat64(min(low))   AS bar_low
    FROM global_markets.stocks_daily_aggs
    WHERE ticker = 'NVDA'
      AND date >= '2024-11-01'
      AND date <= '2025-06-30'
    GROUP BY date
),
marked AS
(
    SELECT
        date,
        bar_close,
        max(bar_close) OVER (ORDER BY date ASC ROWS BETWEEN 20 PRECEDING AND 1 PRECEDING) AS prior_peak_close,
        count()        OVER (ORDER BY date ASC ROWS BETWEEN 20 PRECEDING AND 1 PRECEDING) AS prior_bars,
        lagInFrame(bar_close, 10) OVER (ORDER BY date ASC ROWS BETWEEN 10 PRECEDING AND CURRENT ROW) AS close_10_back,
        avg((bar_high - bar_low) / bar_close) OVER (ORDER BY date ASC ROWS BETWEEN 9 PRECEDING AND CURRENT ROW) AS mean_range
    FROM daily
)
SELECT
    toString(date)                                                                              AS date,
    concat(substring(monthName(toDate(date)), 1, 3), ' ', toString(toDayOfMonth(toDate(date)))) AS session_label,
    round(100 * (bar_close / close_10_back - 1), 2)                                             AS run_up_pct,
    round(100 * mean_range, 2)                                                                  AS avg_range_pct
FROM marked
WHERE prior_bars = 20
  AND close_10_back > 0
  AND bar_close > prior_peak_close
  AND date >= '2025-01-02'
ORDER BY date ASC
Run this yourself

The panel marks every session in the first half of 2025 that closed at a new 20-session high for NVDA, 21 of them, with the ten-session move into each and the average daily range over those sessions. The first, Jan 6, arrived on a move of 14.35% with an average range of 3.35%. The last, Jun 30, measured 11.28% and 1.98%. A run of higher highs where that move series steps down is the shape a reader is looking at when calling a divergence, and calling it is not the same as being right about what follows.

How these panels simplify the rules
  • The containment and turn panels count raw bars; a real implementation merges first, so read them as comparisons of scale rather than a fractal census.
  • Bars at each size are cut on clock slots within regular trading hours, so a few turns straddle a session boundary.
  • The pivot panel intersects three consecutive daily bars where Chanlun intersects three consecutive segments.
  • The divergence panel measures a ten-session move into each new high, standing in for the MACD area a reader would compare.

What makes Chanlun different from Western pattern trading

Construction is procedural rather than visual: a head and shoulders is recognised by eye and argued over, while a stroke is the output of a stated procedure that can be audited line by line. It is a hierarchy of levels rather than one chart, so a reading at one level is checked against the level above or below. And the level comes before the trade: the invalidation price is read off structure that had already printed.

Deterministic construction is not the same as predictive

None of this makes Chanlun predictive. Deterministic construction means an implementation can be tested for correctness against a fixed bar series, and two runs over the same input agree. It says nothing about the outcome of trades taken from the structure, and this page takes no position on that.

What reproducibility does buy is an honest test. Structure methods leak the future very easily. A fractal at one bar is confirmed only at the next, a stroke can be revoked when the following fractal fails the length rule, and a segment can be rewritten long after it began. A backtest that draws the full structure across the whole history and then walks a simulated trader through it has already seen the ending. Look-ahead bias in backtesting covers that failure mode and how a bar-by-bar test avoids peeking.

That the rules can be mechanised is not in question. chan-lun, an MIT-licensed Python library, ships the whole ladder at tag v0.1.1 (August 2026): bar normalisation, fractals and strokes, segments, pivots, divergence, and a cross-level check reading a daily view against 30-minute structure. It takes a plain sequence of bars and carries no market-data connection of its own, so the same input yields the same drawing every time. That input can be A-shares, where the method is most widely used, or US equities; either way it is a bar table, and the local A-share market data lake post covers assembling one.

FAQ

What does Chanlun mean?

Chanlun (缠论) translates roughly as "Chan's theory", after the pen name of the author who published the original lessons on a Chinese blog in the second half of the 2000s.

Is Chanlun the same as Elliott Wave?

No. Both describe markets as nested moves at multiple scales. The difference is procedural: Elliott wave counts are assigned by an analyst and revised often, while Chanlun specifies the construction of every element, from bar merging to the pivot, tightly enough for code to reproduce it.

What is a pivot in Chanlun?

A pivot (中枢) is the overlapping price band shared by three consecutive same-level moves, running from the highest of their lows to the lowest of their highs. It anchors trend classification at that level and supplies the invalidation price a reader writes down in advance.

Can Chanlun be automated?

Up to the divergence step, yes. Bar merging, fractals, strokes, segments and pivots are all defined procedurally, and open-source libraries implement them. The strength comparison at a divergence needs a convention chosen by the implementer, which is where two libraries reading identical bars part company.


Every panel above ships the SQL that produced it. To rebuild any of these counts on a different name or window, ask the question in plain English on the Strasmore terminal.

#technical analysis#chanlun#chart structure#open source#china markets