Discover How a Simple Candle Pattern Strategy Achieved a 65% Win Rate in Backtests | by Aydar Murt | The Capital | Jan, 2025
Before implementing any trading strategy, preparing clean, structured data is crucial. Below is the Python script for managing this vital step:
import pandas as pd
import pandas_ta as ta
from tqdm import tqdm
import os
import numpy as np
import plotly.graph_objects as gotqdm.pandas()
def read_csv_to_dataframe(file_path):
df = pd.read_csv(file_path)
df["Gmt time"] = df["Gmt time"].str.replace(".000", "")
df['Gmt time'] = pd.to_datetime(df['Gmt time'], format='%d.%m.%Y %H:%M:%S')
df = df[df.High != df.Low] # Remove invalid rows
df.set_index("Gmt time", inplace=True)
return df
def read_data_folder(folder_path="./data"):
dataframes = []
file_names = []
for file_name in tqdm(os.listdir(folder_path)):
if file_name.endswith('.csv'):
file_path = os.path.join(folder_path, file_name)
df = read_csv_to_dataframe(file_path)
dataframes.append(df)
file_names.append(file_name)
return dataframes, file_names
Step Breakdown:
- Library Importation: Libraries such as
pandas
streamline data manipulation,pandas_ta
calculates technical indicators,tqdm
monitors progress, andplotly
is used for visualization. - File Preprocessing: Files are read individually, and irrelevant rows are filtered out (e.g., rows where
High
equalsLow
are considered anomalies). - Timestamp Conversion: The datetime format is standardized to ensure consistent time-series indexing.
- Efficiency for Bulk Data: The
read_data_folder
function enables processing multiple datasets, accommodating scenarios like multi-asset analysis.
The strategy relies on specific conditions met by sequential candlestick formations. Here is the function that evaluates the pattern:
def total_signal(df, current_candle):
current_pos = df.index.get_loc(current_candle)
c1 = df['High'].iloc[current_pos] > df['Close'].iloc[current_pos]
c2 = df['Close'].iloc[current_pos] > df['High'].iloc[current_pos-2]
c3 = df['High'].iloc[current_pos-2] > df['High'].iloc[current_pos-1]
c4 = df['High'].iloc[current_pos-1] > df['Low'].iloc[current_pos]
c5 = df['Low'].iloc[current_pos] > df['Low'].iloc[current_pos-2]
c6 = df['Low'].iloc[current_pos-2] > df['Low'].iloc[current_pos-1]
if c1 and c2 and c3 and c4 and c5 and c6:
return 2 # Signal to buy (long)
# Symmetrical conditions for short signals
c1 = df['Low'].iloc[current_pos] < df['Open'].iloc[current_pos]
c2 = df['Open'].iloc[current_pos] < df['Low'].iloc[current_pos-2]
c3 = df['Low'].iloc[current_pos-2] < df['Low'].iloc[current_pos-1]
c4 = df['Low'].iloc[current_pos-1] < df['High'].iloc[current_pos]
c5 = df['High'].iloc[current_pos] < df['High'].iloc[current_pos-2]
c6 = df['High'].iloc[current_pos-2] < df['High'].iloc[current_pos-1]
if c1 and c2 and c3 and c4 and c5 and c6:
return 1 # Signal to sell (short)
return 0
Step Breakdown:
- Logic Explanation: Six conditions ensure precise detection of a specific candlestick sequence, defining entry points for both buy and sell signals.
- Signal Output: The function returns a numerical signal (
2
for long,1
for short,0
for no signal), which will be later interpreted during backtesting. - Error Minimization: By using strict logical conditions, false signals are minimized, ensuring the strategy operates on high-probability setups.
Visualizing entry and exit points is essential for verifying a strategy. Below are the functions for marking patterns and plotting them on candlestick charts:
def add_total_signal(df):
df['TotalSignal'] = df.progress_apply(lambda row: total_signal(df, row.name), axis=1)
return dfdef add_pointpos_column(df, signal_column):
def pointpos(row):
if row[signal_column] == 2:
return row['Low'] - 1e-4
elif row[signal_column] == 1:
return row['High'] + 1e-4
return np.nan
df['pointpos'] = df.apply(lambda row: pointpos(row), axis=1)
return df
def plot_candlestick_with_signals(df, start_index, num_rows):
df_subset = df[start_index:start_index + num_rows]
fig = make_subplots(rows=1, cols=1)
fig.add_trace(go.Candlestick(
x=df_subset.index,
open=df_subset['Open'],
high=df_subset['High'],
low=df_subset['Low'],
close=df_subset['Close'],
name='Candlesticks'), row=1, col=1)
fig.add_trace(go.Scatter(
x=df_subset.index, y=df_subset['pointpos'], mode="markers",
marker=dict(size=10, color="MediumPurple", symbol='circle'),
name="Entry Points"), row=1, col=1)
fig.show()
Step Breakdown:
- Integration with Signals: The
add_total_signal
function augments the dataset with generated trade signals. - Chart Enhancements: Entry points are marked as purple circles below or above candles, aligning visual feedback with strategy logic.
- Customizability: The script accommodates customizations such as time ranges, making it adaptable for diverse datasets.
With the logic in place, it’s time to backtest. Here’s how to evaluate performance across various assets:
from backtesting import Strategy, Backtestdef SIGNAL():
return df.TotalSignal
class MyStrat(Strategy):
mysize = 0.1 # Trade size
slperc = 0.04 # Stop loss percentage
tpperc = 0.02 # Take profit percentage
def init(self):
self.signal1 = self.I(SIGNAL)
def next(self):
if self.signal1 == 2 and not self.position:
self.buy(size=self.mysize, sl=self.data.Close[-1] * (1 - self.slperc), tp=self.data.Close[-1] * (1 + self.tpperc))
elif self.signal1 == 1 and not self.position:
self.sell(size=self.mysize, sl=self.data.Close[-1] * (1 + self.slperc), tp=self.data.Close[-1] * (1 - self.tpperc))
Step Breakdown:
- Stop Loss & Take Profit Levels: Adjustable percentages ensure robust risk management.
- Signal Integration: The backtest dynamically interprets generated signals and executes trades accordingly.
- Flexibility for Optimization: Parameters like
mysize
,slperc
, andtpperc
can be fine-tuned to maximize profitability.
Backtesting results on the S&P 500 index offered the following takeaways:
- 65% Win Rate: Demonstrates consistent success under various market conditions.
- Aggregated Return: Yielded a notable 71% return across the testing period.
- Trade-Specific Insights: The best trade delivered a profit of 10.3%, while the worst drawdown was limited to 4.8%.
While this strategy excelled with equities, its performance on forex was less consistent, indicating potential for asset-specific refinement.
This candlestick pattern strategy demonstrates the power of blending traditional trading wisdom with cutting-edge automation. While its simplicity is an advantage, combining it with additional patterns or refining it for specific asset classes may unlock greater potential.
For step-by-step tutorials and Python code, visit our newsletter, where we dive even deeper into algorithmic strategies and trading technologies. Subscribe to stay updated with the latest advancements in automated trading!