Implementing a Nofri Congestion Phase Trading System in Python: A robust strategy for identifying and trading in Sideways Markets

Suyash Khare
6 min readJul 20, 2023

--

Introduction:

In the world of financial markets, traders often seek strategies that can identify potential trading opportunities based on certain market conditions. One such strategy is the Nofri Congestion Phase Trading System, which aims to identify periods of consolidation in the price movement before potential breakouts or trend reversals. In this article, we will explore a Python implementation of the Congestion Phase Trading System.

The Nofri Congestion Phase Trading System

The Nofri Congestion Phase Trading System is a trading strategy designed by trader and author Raffaele Nofri. This system aims to identify periods of price consolidation or congestion in the financial markets and capitalize on potential breakout opportunities. It is based on the observation that markets often go through phases of low volatility and narrow price ranges before experiencing significant price moves.

The main idea behind the Nofri Congestion Phase Trading System is to enter positions during congestion periods and hold them until the price breaks out of the consolidation range. The strategy seeks to avoid trading during trending or volatile market conditions and focuses on capturing profits from the potential expansion of price movement after a period of consolidation.

Key Components and Rules:

  1. Average True Range (ATR): ATR is used to measure market volatility and set the size of stop-loss orders or profit targets. It helps determine the potential price range within which the market might move during a given period.
  2. Congestion Areas: The strategy identifies congestion areas or periods of price consolidation. These are characterized by relatively tight price ranges and low volatility. The system uses specific price patterns to identify congestion highs and congestion lows.
  3. False Breakouts: The strategy acknowledges that breakouts from congestion areas are not always genuine and can lead to false signals. To address this, the system waits for a specific number of days after a breakout before entering a trade, reducing the chances of getting caught in a false breakout.
  4. Entry Signals: The Nofri Congestion Phase Trading System generates entry signals based on certain price patterns observed during congestion areas. These patterns provide cues for potential breakout directions.
  5. Position Sizing: The system determines the size of the trading position based on the ATR value. The position size is adjusted to account for market volatility and risk.
  6. Exit Signals: The system uses various exit signals to close existing positions. These may include reaching predefined profit targets, stop-loss levels, or specific price patterns indicating a potential trend reversal.

Pros and Cons:

Pros:

  • The Nofri Congestion Phase Trading System aims to capitalize on potential price expansions after periods of low volatility, which can lead to significant profits.
  • It incorporates risk management techniques, such as using ATR-based position sizing and waiting periods after false breakouts, to mitigate potential losses.

Cons:

  • The system may generate false signals or struggle during extended periods of sideways markets.
  • Like any trading strategy, it carries inherent risks, and historical performance does not guarantee future results.

Understanding the Code:

The code consists of two main functions: atr() and TSM5_Nofris_Congestion_Phase_System(). Let's break down the code step by step and understand its components.

def atr(data, period=10):
high = data['High']
low = data['Low']
close = data['Close']
hl = high - low
hc = abs(high - close.shift())
lc = abs(low - close.shift())
tr = np.maximum(hl, hc)
tr = np.maximum(tr, lc)
atr = tr.rolling(window=period).mean()
return atr

The atr() Function:

The first function, atr(data, period=10), calculates the Average True Range (ATR) for a given dataset. ATR is a measure of market volatility and is often used to determine the size of stop-loss orders or profit targets. The function takes a Pandas DataFrame named data as input, which should contain columns for 'High', 'Low', and 'Close' prices. The 'period' parameter specifies the number of periods to consider when calculating the rolling average.

def TSM5_Nofris_Congestion_Phase_System(data):
# Constants and parameters
daysinfalsebreakout = 3
multipleofavgmove = 2
daysinlargemove = 10
entryrule = 2
investment = 25000

# Variables initialization
highisactive = False
lowisactive = False
wait = 0
dayssincebreakout = 0
debug = True
congestionhigh = 0
congestionlow = 0
netmove = 0
avgmove = 0
prevhigh = 0
prevlow = 0
size = 0
pmarketposition = 0

# Data preparation
data['dayssincebreakout'] = 0
data['wait'] = 0
data['highisactive'] = False
data['lowisactive'] = False
data['congestionhigh'] = 0
data['congestionlow'] = 0
data['netmove'] = 0
data['avgmove'] = 0
data['prevhigh'] = 0
data['prevlow'] = 0

for i in range(1, len(data)):
# Calculate net move and average move
data['netmove'].iloc[i] = data['netmove'].iloc[i-1] + abs(data['Close'].iloc[i] - data['Close'].iloc[i - daysinlargemove])
if i > 2 * daysinlargemove:
data['avgmove'].iloc[i] = data['netmove'].iloc[i] / i
else:
data['avgmove'].iloc[i] = 0

# Wait 10 days if a congestion area follows a large price move
if data['MarketPosition'].iloc[i] != 0 and data['netmove'].iloc[i] > multipleofavgmove * data['avgmove'].iloc[i - 1]:
data['wait'].iloc[i] = 10

# Congestion area high
if data['High'].iloc[i - 2] > data['High'].iloc[i - 3] and data['Close'].iloc[i - 1] < data['Close'].iloc[i - 2] and data['Close'].iloc[i] < data['Close'].iloc[i - 1]:
data['highisactive'].iloc[i] = True
data['congestionhigh'].iloc[i] = data['High'].iloc[i - 2]
if data['Low'].iloc[i] < data['prevlow'].iloc[i] and data['dayssincebreakout'].iloc[i] >= daysinfalsebreakout:
data['wait'].iloc[i] = 7

# Congestion area low
if data['Low'].iloc[i - 2] < data['Low'].iloc[i - 3] and data['Close'].iloc[i - 1] > data['Close'].iloc[i - 2] and data['Close'].iloc[i] > data['Close'].iloc[i - 1]:
data['lowisactive'].iloc[i] = True
data['congestionlow'].iloc[i] = data['Low'].iloc[i - 2]
if data['High'].iloc[i] > data['prevhigh'].iloc[i] and data['dayssincebreakout'].iloc[i] >= daysinfalsebreakout:
data['wait'].iloc[i] = 7

# End of congestion area
if data['highisactive'].iloc[i] and data['High'].iloc[i] > data['congestionhigh'].iloc[i]:
data['prevhigh'].iloc[i] = data['congestionhigh'].iloc[i]
data['dayssincebreakout'].iloc[i] = 1
data['highisactive'].iloc[i] = False
if data['lowisactive'].iloc[i] and data['Low'].iloc[i] < data['congestionlow'].iloc[i]:
data['prevlow'].iloc[i] = data['congestionlow'].iloc[i]
data['dayssincebreakout'].iloc[i] = 1
data['lowisactive'].iloc[i] = False

# Close out any existing trades on the next close
if data['MarketPosition'].iloc[i - 1] > 0:
data['SellSignal'].iloc[i] = True
elif data['MarketPosition'].iloc[i - 1] < 0:
data['BuySignal'].iloc[i] = True

# Entry signal
if data['highisactive'].iloc[i] and data['lowisactive'].iloc[i] and data['congestionhigh'].iloc[i] > data['congestionlow'].iloc[i]:
size = investment / atr(data.iloc[i-daysinlargemove:i], daysinlargemove).iloc[-1]
if entryrule == 2:
if data['Close'].iloc[i] > data['Close'].iloc[i - 1] and data['Close'].iloc[i - 1] > data['Close'].iloc[i - 2]:
data['SellShortSignal'].iloc[i] = True
elif data['Close'].iloc[i] < data['Close'].iloc[i - 1] and data['Close'].iloc[i - 1] < data['Close'].iloc[i - 2]:
data['BuySignal'].iloc[i] = True
elif entryrule == 3:
if data['Close'].iloc[i] > data['Close'].iloc[i - 1] and data['Close'].iloc[i - 1] > data['Close'].iloc[i - 2] and data['Close'].iloc[i - 2] > data['Close'].iloc[i - 3]:
data['SellShortSignal'].iloc[i] = True
elif data['Close'].iloc[i] < data['Close'].iloc[i - 1] and data['Close'].iloc[i - 1] < data['Close'].iloc[i - 2] and data['Close'].iloc[i - 2] < data['Close'].iloc[i - 3]:
data['BuySignal'].iloc[i] = True

# Update previous market position
data['pmarketposition'].iloc[i] = data['MarketPosition'].iloc[i]

# Print details for debugging (continued)
if i == 1:
print("Date,High,Low,Close,newhigh,newlow,congestionhigh,congestionlow,marketposition,daysbkout,wait,openPL,netPL")

print(f"{data['Date'].iloc[i]},{data['High'].iloc[i]:.4f},{data['Low'].iloc[i]:.4f},{data['Close'].iloc[i]:.4f},{data['highisactive'].iloc[i]},{data['lowisactive'].iloc[i]},"
f"{data['congestionhigh'].iloc[i]:.4f},{data['congestionlow'].iloc[i]:.4f},{data['MarketPosition'].iloc[i]:.0f},{data['dayssincebreakout'].iloc[i]:.0f},"
f"{data['wait'].iloc[i]:.0f},{data['OpenPositionProfit'].iloc[i]:.0f},{data['NetProfit'].iloc[i]:.0f}")

return data

The TSM5_Nofris_Congestion_Phase_System() Function:

This is the main function that implements the Congestion Phase Trading System. It takes a Pandas DataFrame named data as input, which should contain historical price data and additional columns for tracking various parameters related to the trading strategy.

The function begins by initializing several constants, parameters, and variables needed for the trading strategy. These include ‘daysinfalsebreakout’, ‘multipleofavgmove’, ‘daysinlargemove’, ‘entryrule’, and ‘investment’. These parameters control various aspects of the trading system, such as the number of days to wait after a false breakout, the multiple of average move to consider for waiting, the number of days to consider for a large price move, the entry rule for opening positions, and the initial investment amount, respectively.

Next, the function performs data preparation by adding new columns to the DataFrame for storing various intermediate results and parameters of the trading strategy.

The core of the strategy is implemented in a loop that iterates through each row of the DataFrame starting from the second row (index 1). The loop calculates the ‘netmove’ and ‘avgmove’ values, which represent the cumulative and average price movements, respectively, over a specified number of days (daysinlargemove). The ‘netmove’ and ‘avgmove’ values are used to determine whether the market is experiencing congestion or significant price moves.

The function then identifies congestion areas based on specific price patterns and conditions. For example, it checks for congestion area highs and lows and sets flags (‘highisactive’ and ‘lowisactive’) to indicate when these areas are active. It also keeps track of the number of days since the last breakout to identify false breakouts.

If the congestion area follows a large price move, the function sets a wait period to avoid trading immediately after significant price fluctuations.

The function also checks for potential entry signals based on certain price patterns, and if an entry rule is met, it generates buy or sell short signals. The size of the position is determined based on the ATR value calculated using the atr() function.

Finally, the function updates the previous market position and prints some debugging information for each iteration.

Conclusion:

The provided code demonstrates a basic implementation of the Congestion Phase Trading System using Python’s Pandas and Numpy libraries. It is essential to note that this implementation is for educational purposes and may require further refinement and optimization before using it in real trading scenarios.

Before using any trading strategy in the live markets, it is crucial to thoroughly backtest the strategy on historical data, consider transaction costs, slippage, and other real-world factors that may impact the strategy’s performance. Additionally, trading strategies always carry inherent risks, and no strategy can guarantee profits. It is advisable to consult with financial professionals and conduct thorough research before engaging in any trading activities.

--

--

Suyash Khare
Suyash Khare

Written by Suyash Khare

They call me Dirichlet because all my potential is latent and awaiting allocation.

No responses yet