Hello! Welcome back to our module on "Automating Core ICT Concepts."
In our last lesson, we built a Python script to identify a Break of Structure (BOS), the key event that confirms a trend is continuing. You learned how to track swing points and detect when price breaks through them, which is fundamental for any trend-following logic.
Today, we'll tackle the other side of the coin. If a BOS signals continuation, how do we spot the first sign of a potential reversal? This brings us to our learning outcome: to implement an algorithm to programmatically identify a change of character (ChoCH). A ChoCH is your earliest warning that the prevailing trend might be losing steam and preparing to reverse. For your goal of building trading bots, this is a critical component for capturing reversals or exiting trades before a trend turns against you.
1. What is a Change of Character (ChoCH)?
While a Break of Structure (BOS) breaks an external swing point in the direction of the trend, a Change of Character (ChoCH) is the first time price breaks a key internal swing point against the trend.
- Bearish ChoCH: In an uptrend, the market creates Higher Highs (HH) and Higher Lows (HL). A bearish ChoCH occurs when price fails to make a new HH and instead breaks below the most recent Higher Low.
- Bullish ChoCH: In a downtrend, the market creates Lower Lows (LL) and Lower Highs (LH). A bullish ChoCH occurs when price fails to make a new LL and instead breaks above the most recent Lower High.
This image clearly illustrates the concept.

To solidify this foundational concept, the following video provides an excellent, in-depth explanation.
How to Identify Change of Character (CHoCH) - Smart Money Concepts
The video 'How to Identify Change of Character (CHoCH)' from the Camelthorn FX channel provides a clear, rule-based framework for identifying a valid ChoCH.
Please watch two segments: From the beginning to 03:19 to understand how ChoCH fits into the broader context of market structure. From 04:37 to 06:30 to learn the specific rules for identifying a valid ChoCH. Pay close attention to the idea of a break with the trend immediately followed by a break against it.
As the video explains, the key is the violation of the swing point that was responsible for the last push in the trend's direction. Breaking this level "changes the character" of the price action from trending to potentially reversing.
2. From Concept to Algorithm
Let's translate this into a step-by-step process that a computer can follow. Our algorithm needs to keep track of the current trend and the key swing point that, if broken, would invalidate that trend. We can build directly on the functions from our previous lessons.
Our Algorithm:
- Start with a DataFrame that has
is_swing_high,is_swing_low, andboscolumns already identified. Theboscolumn will help us determine the current trend direction. - Initialize variables to track the current
trend(e.g.,1for bullish,-1for bearish) and the critical swing points that define the ChoCH levels:choch_level_up(the last lower high in a downtrend) andchoch_level_down(the last higher low in an uptrend). - Iterate through the DataFrame candle by candle:
- If a bullish BOS occurs, set
trend = 1. - If a bearish BOS occurs, set
trend = -1. - When the
trendis bullish (1) and a new swing low forms, this is our new Higher Low. Update thechoch_level_downto this low's price. This is the level that must hold for the uptrend to continue. - When the
trendis bearish (-1) and a new swing high forms, this is our new Lower High. Update thechoch_level_upto this high's price. This is the level that must hold for the downtrend to continue.
- If a bullish BOS occurs, set
- Detect the ChoCH:
- If
trendis bullish and the current candle's close breaks belowchoch_level_down, we have a bearish ChoCH. Mark it and reset the trend. - If
trendis bearish and the current candle's close breaks abovechoch_level_up, we have a bullish ChoCH. Mark it and reset the trend.
- If
This logic directly mirrors the ICT definition and is something we can now translate into Python.
3. Implementing the ChoCH Function in Python
We will now create a find_choch function. It will take the DataFrame produced by our previous find_bos function as its input. This chaining of functions is common in algorithmic trading, where you progressively add more layers of analysis to your data.
import pandas as pd
import yfinance as yf
import plotly.graph_objects as go
# --- Reusing functions from previous lessons ---
def find_swings(df: pd.DataFrame, n: int = 5):
df_copy = df.copy()
window_size = 2 * n + 1
df_copy['rolling_max'] = df_copy['High'].rolling(window=window_size, center=True).max()
df_copy['rolling_min'] = df_copy['Low'].rolling(window=window_size, center=True).min()
df_copy['is_swing_high'] = (df_copy['High'] == df_copy['rolling_max'])
df_copy['is_swing_low'] = (df_copy['Low'] == df_copy['rolling_min'])
df_copy.drop(columns=['rolling_max', 'rolling_min'], inplace=True)
return df_copy
def find_bos(df_with_swings: pd.DataFrame):
df = df_with_swings.copy()
df['bos'] = 0
last_swing_high = None
last_swing_low = None
for i in range(len(df)):
if df['is_swing_high'].iloc[i]:
last_swing_high = df['High'].iloc[i]
if df['is_swing_low'].iloc[i]:
last_swing_low = df['Low'].iloc[i]
if last_swing_high is not None and df['Close'].iloc[i] > last_swing_high:
df.loc[df.index[i], 'bos'] = 1
last_swing_high = None
if last_swing_low is not None and df['Close'].iloc[i] < last_swing_low:
df.loc[df.index[i], 'bos'] = -1
last_swing_low = None
return df
# --- New function for this lesson ---
def find_choch(df_with_bos: pd.DataFrame):
"""
Identifies Change of Character (ChoCH) based on trend and swing points.
Args:
df_with_bos: DataFrame with 'is_swing_high', 'is_swing_low', and 'bos' columns.
Returns:
The DataFrame with a 'choch' column (1 for bullish, -1 for bearish, 0 for none).
"""
df = df_with_bos.copy()
df['choch'] = 0
trend = 0 # 1 for bullish, -1 for bearish
choch_level_up = None # The last Lower High in a downtrend
choch_level_down = None # The last Higher Low in an uptrend
for i in range(len(df)):
# Update trend based on BOS
if df['bos'].iloc[i] == 1:
trend = 1
choch_level_up = None # Invalidate opposite level on BOS
elif df['bos'].iloc[i] == -1:
trend = -1
choch_level_down = None # Invalidate opposite level on BOS
# Update ChoCH levels based on new swings within a trend
if trend == 1 and df['is_swing_low'].iloc[i]:
choch_level_down = df['Low'].iloc[i]
elif trend == -1 and df['is_swing_high'].iloc[i]:
choch_level_up = df['High'].iloc[i]
# Check for bearish ChoCH
if trend == 1 and choch_level_down is not None:
if df['Close'].iloc[i] < choch_level_down:
df.loc[df.index[i], 'choch'] = -1
trend = -1 # Trend has now changed
choch_level_down = None # Invalidate the level after it's broken
# Check for bullish ChoCH
if trend == -1 and choch_level_up is not None:
if df['Close'].iloc[i] > choch_level_up:
df.loc[df.index[i], 'choch'] = 1
trend = 1 # Trend has now changed
choch_level_up = None # Invalidate the level after it's broken
return df
# --- Example Usage ---
# 1. Fetch data
data = yf.download('EURUSD=X', period='90d', interval='4h')
# 2. Find swings
data_with_swings = find_swings(data, n=5)
# 3. Find BOS
data_with_bos = find_bos(data_with_swings)
# 4. Find ChoCH
data_with_choch = find_choch(data_with_bos)
# Let's see the candles where a ChoCH was detected
print("ChoCH Detected:")
print(data_with_choch[data_with_choch['choch'] != 0])
Test your understanding!
In our algorithm, a bullish BOS confirms an uptrend, and we then start tracking the choch_level_down (the most recent swing low). A bearish ChoCH is signaled when price closes below this level.
Why does our algorithm for a bearish ChoCH look for a break of a low, while our algorithm for a bullish BOS looks for a break of a high?
Show answer
This is the core difference between the two concepts.
- A bullish BOS signals the continuation of an uptrend by overpowering the last point of resistance (the last swing high).
- A bearish ChoCH signals a potential reversal of an uptrend by failing to hold the last point of support (the last swing low).
So, even though both events might happen during an uptrend, they are defined by breaking opposite sides of the market structure.
4. Visualizing BOS and ChoCH Together
The best way to see the difference is to plot both signals on the same chart. This will clearly show you how the market continues with BOS and signals a potential turn with ChoCH. Let's adapt our plotting function.
def plot_structure(df: pd.DataFrame):
fig = go.Figure()
fig.add_trace(go.Candlestick(x=df.index, open=df['Open'], high=df['High'], low=df['Low'], close=df['Close'], name='Candles'))
# Plot Swings
swing_highs = df[df['is_swing_high']]
swing_lows = df[df['is_swing_low']]
fig.add_trace(go.Scatter(x=swing_highs.index, y=swing_highs['High'], mode='markers', marker=dict(symbol='triangle-down', color='red', size=8), name='Swing High'))
fig.add_trace(go.Scatter(x=swing_lows.index, y=swing_lows['Low'], mode='markers', marker=dict(symbol='triangle-up', color='lime', size=8), name='Swing Low'))
# Plot BOS
bullish_bos = df[df['bos'] == 1]
bearish_bos = df[df['bos'] == -1]
fig.add_trace(go.Scatter(x=bullish_bos.index, y=bullish_bos['High'], mode='markers', marker=dict(symbol='circle', color='aqua', size=10, line=dict(width=2, color='DarkSlateGrey')), name='Bullish BOS'))
fig.add_trace(go.Scatter(x=bearish_bos.index, y=bearish_bos['Low'], mode='markers', marker=dict(symbol='circle', color='magenta', size=10, line=dict(width=2, color='DarkSlateGrey')), name='Bearish BOS'))
# Plot ChoCH
bullish_choch = df[df['choch'] == 1]
bearish_choch = df[df['choch'] == -1]
fig.add_trace(go.Scatter(x=bullish_choch.index, y=bullish_choch['High'], mode='markers', marker=dict(symbol='star', color='gold', size=12), name='Bullish ChoCH'))
fig.add_trace(go.Scatter(x=bearish_choch.index, y=bearish_choch['Low'], mode='markers', marker=dict(symbol='star', color='orange', size=12), name='Bearish ChoCH'))
fig.update_layout(title='Market Structure: Swings, BOS, and ChoCH', template='plotly_dark', xaxis_rangeslider_visible=False)
fig.show()
# --- Use the function to plot our final results ---
plot_structure(data_with_choch)
When you run this code, examine the chart. Notice how BOS markers appear during a clear trend, while ChoCH markers often appear at the turning points, right before the trend reverses and a series of opposite BOS markers begin.
5. The Efficient Path: Using the smartmoneyconcepts Library
Now that you've built the logic from the ground up, you can appreciate the simplicity of using a specialized library. The smartmoneyconcepts package we've been using has a single function that detects both BOS and ChoCH.
The PyPI documentation for smartmoneyconcepts outlines the bos_choch function, which we can use to detect both structural events at once.
Find the heading 'Break of Structure (BOS) & Change of Character (CHoCH)'. Notice that the function returns columns for both BOS and CHOCH, making it very efficient.
Using the library is straightforward and condenses our multi-function process into a few lines.
from smartmoneyconcepts import smc
# The library expects lowercase column names
data_smc = data.copy()
data_smc.columns = [col.lower() for col in data_smc.columns]
# 1. First, find the swings (as required by the bos_choch function)
swing_points = smc.swing_highs_lows(data_smc, swing_length=5)
# 2. Then, find BOS and ChoCH in one step
bos_choch_df = smc.bos_choch(data_smc, swing_highs_lows=swing_points, close_break=True)
# The result is a DataFrame with 'BOS' and 'CHOCH' columns
print("ChoCH detected by library:")
print(bos_choch_df[bos_choch_df['CHOCH'] != 0])
While the library is faster, writing the logic yourself ensures you understand exactly what the tool is doing. You could even modify your custom function to use different rules (e.g., wick breaks instead of closes) based on your trading plan.
Conclusion
Excellent work today! You have now automated the detection of the two most important market structure events: continuation (BOS) and potential reversal (ChoCH).
Key Takeaways:
- A Change of Character (ChoCH) is the first sign of a potential trend reversal, occurring when an internal swing point against the trend is broken.
- A bearish ChoCH breaks the last higher low in an uptrend, while a bullish ChoCH breaks the last lower high in a downtrend.
- The algorithm to detect a ChoCH relies on first establishing the trend (e.g., using BOS signals) and then monitoring the key internal swing levels that protect that trend.
- Visualizing BOS and ChoCH on the same chart clarifies their distinct roles in price action.
- Libraries like
smartmoneyconceptsoffer a highly efficient way to implement this logic once you understand the underlying principles.
Preview for the Next Lesson:
We've now built a solid foundation in automating market structure. Next, we will move on to another core ICT pillar: market imbalances. In the next lesson, you will learn to write Python logic to detect Fair Value Gaps (FVGs) from OHLC data. These gaps act as powerful magnets for price and are crucial for identifying high-probability entry and exit zones.