Systematic 5-Min Pin Bar Reversal (Hammer & Shooting Star)

🦊 Premium Script

Strategy Overview: The Systematic 5-Min Pin Bar Reversal strategy is a highly precise, candlestick-driven algorithmic framework designed to capture violent market rejections at key intraday extremes. Operating on a 5-minute timeframe, this system mathematically isolates ‘Hammer’ and ‘Shooting Star’ formations by analyzing the precise ratio of the candle’s body to its wicks. A bullish Hammer is detected when the lower wick is at least twice the size of the body during a short-term downtrend, signaling aggressive institutional buying and triggering a long entry. Conversely, a bearish Shooting Star is identified when a massive upper wick forms during an uptrend, revealing a sudden lack of demand and executing a short position. To filter out market noise, the algorithm mandates that the candle body exceeds a minimum point threshold, effectively ignoring flat dojis and low-volume chop. Local trend confirmation is strictly enforced by comparing the current closing price to the close from three periods prior, ensuring reversals are only traded against established momentum. Risk management is hard-coded into the execution logic, utilizing a strict 50-point stop loss to truncate drawdowns and a 100-point take-profit to secure alpha. This mechanical structure guarantees a highly asymmetric 1:2 risk-to-reward ratio on every single trade, allowing the system to remain profitable even with a sub-50% win rate. Additionally, the system features a built-in reversal protocol that flips the directional bias if an opposing pin bar formation immediately materializes. As a purely intraday quantitative model, it strictly enforces an automated 15:15 square-off sequence, completely neutralizing the portfolio against unpredictable overnight gaps and theta decay.

📈 EXPECTED WIN RATE
47.07%
📉 MAX DRAWDOWN
817 Pts
💰 TOTAL PNL
+1,224.0 Pts
🔄 TOTAL TRADES
2,547
🏆 TOTAL WINS
1,199
!
Important This algorithm dynamically calculates candlestick anatomy (wicks vs. bodies) using pandas max(axis=1) and min(axis=1) functions, whilst verifying the local trend by comparing current prices to the shift(3) lookback period.
ℹ️
Disclaimer These results are generated based on automated backtesting performed using Python code and algorithms. Actual results may vary, and manual backtesting outcomes could differ due to varying assumptions, data interpretation, and market conditions. This information is provided for educational and informational purposes only and should not be considered as financial advice. Before making any trading or investment decisions, please consult with a qualified financial advisor or professional.

1. Imports and Configuration

Summary: This section initializes the required libraries and configures the core backtesting setup, including the historical data file path, a 15:15 intraday square-off time, a 5-minute timeframe, and fixed risk parameters of a 50-point stop loss and 100-point target.

python script Copy Code
import pandas as pd
import numpy as np
import datetime as dt

# ===================== CONFIG =====================
FILE_PATH = "Index_1_minute.csv"  
DAYFIRST = True                    
EXIT_TIME = dt.time(15, 15)        
SLIPPAGE = 0.5                     

# Strategy params
TIMEFRAME = "5min"   

# Risk Management
STOP_LOSS_POINTS = 50.0            
TARGET_POINTS = 100.0              
# ==================================================

2. Data Loading and Preparation

Summary: This section loads historical price data from the CSV, validates the presence of required columns, standardizes data types, cleans missing values, and resamples the dataset into structured 5-minute OHLC candles.

python script Copy Code
# ----------------- LOAD & PREP --------------------
print("Loading and preparing data...")
df = pd.read_csv(FILE_PATH)
df.columns = [c.strip().lower() for c in df.columns]
required = {"date","open","high","low","close"}
missing = required - set(df.columns)
if missing:
    raise ValueError(f"CSV missing columns: {missing}. Need exactly {sorted(required)}")

df["dt"] = pd.to_datetime(df["date"], dayfirst=DAYFIRST)
df = df.sort_values("dt").reset_index(drop=True)

for c in ["open","high","low","close"]:
    df[c] = pd.to_numeric(df[c], errors="coerce")
df = df.dropna(subset=["open","high","low","close","dt"]).copy()

# Resampling
df = df.set_index("dt").resample(TIMEFRAME).agg({
    "open": "first",
    "high": "max",
    "low": "min",
    "close": "last"
}).dropna().reset_index()

df["d"] = df["dt"].dt.date
df["t"] = df["dt"].dt.time

3. Candlestick Anatomy & Trend Calculation

Summary: Here, the algorithm dynamically calculates the anatomy of each candlestick. It calculates the size of the candle’s body, upper wick, and lower wick. It also determines the local trend by comparing the current closing price to the close from three periods prior.

python script Copy Code
# ----------------- INDICATORS --------------------
print("Detecting Hammer and Shooting Star (Pin Bar) Patterns...")

# 1. Calculate the components of the candlestick
df["max_oc"] = df[["open", "close"]].max(axis=1)
df["min_oc"] = df[["open", "close"]].min(axis=1)

df["body"] = df["max_oc"] - df["min_oc"]
df["upper_wick"] = df["high"] - df["max_oc"]
df["lower_wick"] = df["min_oc"] - df["low"]

# 2. Local Trend check (Comparing current price to 3 candles ago)
df["trend_down"] = df["close"] < df["close"].shift(3)
df["trend_up"] = df["close"] > df["close"].shift(3)

4. Pattern Recognition Logic

Summary: This applies the mathematical rules to identify valid trade setups. A bullish Hammer requires a lower wick twice the size of the body during a downtrend. A bearish Shooting Star requires an upper wick twice the size of the body during an uptrend. Both filters mandate a minimum body size of 2 points to ignore “doji” market noise.

python script Copy Code
# 3. Hammer (Bullish Reversal) Logic:
# - Lower wick is at least twice the size of the body
# - Upper wick is very small (less than the body)
# - Body is larger than 2 points (to avoid flat dojis)
# - Occurs in a short-term downtrend
df["is_hammer"] = (
    (df["lower_wick"] >= 2.0 * df["body"]) & 
    (df["upper_wick"] <= df["body"]) & 
    (df["body"] > 2.0) & 
    df["trend_down"]
)

# 4. Shooting Star (Bearish Reversal) Logic:
# - Upper wick is at least twice the size of the body
# - Lower wick is very small (less than the body)
# - Body is larger than 2 points
# - Occurs in a short-term uptrend
df["is_shooting_star"] = (
    (df["upper_wick"] >= 2.0 * df["body"]) &
    (df["lower_wick"] <= df["body"]) & 
    (df["body"] > 2.0) & 
    df["trend_up"]
)

5. Utility Functions

Summary: These are helper functions. stats generates the performance summary (win rate, total PnL, etc.) from the list of executed trades. close_out calculates the raw points gained or lost on a specific trade based on its direction.

python script Copy Code
# -------------- UTILITIES ----------------
def stats(trades):
    if not trades:
        return {"trades":0,"wins":0,"win_rate":0.0,"avg_pnl":0.0,"total_pnl":0.0}
    pnl = np.array([x["pnl"] for x in trades])
    wins = (pnl > 0).sum()
    return {
        "trades": len(trades),
        "wins": int(wins),
        "win_rate": round(100*wins/len(trades), 2),
        "avg_pnl": round(pnl.mean(), 2),
        "total_pnl": round(pnl.sum(), 2),
    }

def close_out(direction, entry, row_close):
    return (row_close - entry) if direction=="long" else (entry - row_close)

6. The Core Backtest Engine

Summary: This is the main loop that iterates through the prepared data row by row. It enforces the 15:15 intraday exit, monitors active trades for stop-loss or take-profit hits, initiates new long or short positions based on the candlestick signals, and includes logic to immediately reverse a position if an opposing pattern forms.

python script Copy Code
# -------------- STRATEGY: PIN BAR REVERSAL --------------
def backtest_pin_bar(df):
    print("Running backtest loop...")
    trades = []
    in_trade = False
    direction = None
    entry = None
    entry_time = None

    for _, r in df.iterrows():
        # 1. INTRADAY TIME EXIT
        if r["t"] >= EXIT_TIME:
            if in_trade:
                pnl = close_out(direction, entry, r["close"]) - SLIPPAGE
                trades.append({
                    "date": r["d"], "entry_time": entry_time, "exit_time": r["dt"],
                    "strategy":"PIN_BAR", "dir":direction, "entry":entry,
                    "exit":r["close"], "pnl":pnl, "outcome":"TIME_EXIT"
                })
                in_trade = False
            continue

        if pd.isna(r["is_hammer"]):
            continue

        # 2. SL / TP LOGIC
        if in_trade:
            sl_hit = False
            tp_hit = False
            exit_price = None
            outcome = None

            # Check conservative High/Low hits within the current candle
            if direction == "long":
                if r["low"] <= entry - STOP_LOSS_POINTS:
                    sl_hit = True
                    exit_price = entry - STOP_LOSS_POINTS
                    outcome = "STOP_LOSS"
                elif r["high"] >= entry + TARGET_POINTS:
                    tp_hit = True
                    exit_price = entry + TARGET_POINTS
                    outcome = "TARGET"
            elif direction == "short":
                if r["high"] >= entry + STOP_LOSS_POINTS:
                    sl_hit = True
                    exit_price = entry + STOP_LOSS_POINTS
                    outcome = "STOP_LOSS"
                elif r["low"] <= entry - TARGET_POINTS:
                    tp_hit = True
                    exit_price = entry - TARGET_POINTS
                    outcome = "TARGET"

            # If stopped out or target reached, close the trade
            if sl_hit or tp_hit:
                pnl = close_out(direction, entry, exit_price) - SLIPPAGE
                trades.append({
                    "date": r["d"], "entry_time": entry_time, "exit_time": r["dt"],
                    "strategy":"PIN_BAR", "dir":direction, "entry":entry,
                    "exit":exit_price, "pnl":pnl, "outcome":outcome
                })
                in_trade = False
                direction = None
                entry = None
                entry_time = None
                # Skip entry logic on the same candle we hit SL/TP
                continue

        # Signal Logic
        go_long = r["is_hammer"]
        go_short = r["is_shooting_star"]

        # 3. ENTRY LOGIC
        if not in_trade:
            if go_long:
                direction = "long"
                entry = r["close"]
                entry_time = r["dt"]
                in_trade = True
            elif go_short:
                direction = "short"
                entry = r["close"]
                entry_time = r["dt"]
                in_trade = True

        # 4. REVERSAL LOGIC
        else:
            if direction=="long" and go_short:
                exit_price = r["close"]
                pnl = (exit_price - entry) - SLIPPAGE
                trades.append({
                    "date": r["d"], "entry_time": entry_time, "exit_time": r["dt"],
                    "strategy":"PIN_BAR", "dir":"long", "entry":entry,
                    "exit":exit_price, "pnl":pnl, "outcome":"REVERSE"
                })
                # Reverse to short
                direction = "short"
                entry = r["close"]
                entry_time = r["dt"]

            elif direction=="short" and go_long:
                exit_price = r["close"]
                pnl = (entry - exit_price) - SLIPPAGE
                trades.append({
                    "date": r["d"], "entry_time": entry_time, "exit_time": r["dt"],
                    "strategy":"PIN_BAR", "dir":"short", "entry":entry,
                    "exit":exit_price, "pnl":pnl, "outcome":"REVERSE"
                })
                # Reverse to long
                direction = "long"
                entry = r["close"]
                entry_time = r["dt"]

    return trades

7. Execution and Reporting

Summary: This final section executes the backtest and outputs a concise performance summary to the console. It then organizes detailed trade logs into weekly and monthly breakdowns, computes drawdowns, and exports all results into a structured multi-sheet Excel file (pin_bar_strategy_results.xlsx).

python script Copy Code
# -------------- EXECUTION & REPORTING --------------
pin_trades = backtest_pin_bar(df)
summary = {"PIN_BAR": stats(pin_trades)}

print(f"✅ Backtest complete ({TIMEFRAME} candles)")
for name, s in summary.items():
    print(f"\n{name} -> Trades: {s['trades']}, Wins: {s['wins']}, WinRate: {s['win_rate']}%, "
          f"AvgPnL: {s['avg_pnl']}, TotalPnL: {s['total_pnl']}")

# Write results to Excel
output_file = "pin_bar_strategy_results.xlsx"
print(f"\nSaving results to {output_file}...")

with pd.ExcelWriter(output_file, engine="xlsxwriter") as writer:
    if pin_trades:
        trades_df = pd.DataFrame(pin_trades)
        trades_df.to_excel(writer, sheet_name="PIN_BAR", index=False)

        trades_df["date_dt"] = pd.to_datetime(trades_df["date"])
        trades_df["week"] = trades_df["date_dt"].dt.to_period("W-FRI")
        weekly = trades_df.groupby("week", as_index=False)["pnl"].sum()
        weekly.rename(columns={"pnl": "total_points"}, inplace=True)
        weekly["week"] = weekly["week"].astype(str)
        weekly.to_excel(writer, sheet_name="Weekly_Report", index=False)

        trades_df["month"] = trades_df["date_dt"].dt.to_period("M")
        monthly = trades_df.groupby("month", as_index=False)["pnl"].sum()
        monthly.rename(columns={"pnl": "total_points"}, inplace=True)
        monthly["month"] = monthly["month"].astype(str)
        monthly.to_excel(writer, sheet_name="Monthly_Report", index=False)

        dd_rows = []
        for m, mdf in trades_df.groupby("month"):
            mdf = mdf.sort_values("exit_time")
            cum = mdf["pnl"].cumsum()
            max_dd = cum.min()
            dd_rows.append({"month": str(m), "max_drawdown": max_dd})
        dd_df = pd.DataFrame(dd_rows)
        dd_df.to_excel(writer, sheet_name="Drawdown_Report", index=False)

    pd.DataFrame([{"strategy": k, **v} for k, v in summary.items()]).to_excel(
        writer, sheet_name="Summary", index=False
    )

print("Process finished successfully.")