FabTrader_Logo_Main
  • Home
  • Courses
    • Build Algo Trading Platform using Python Course
  • Store
  • Tools
  • Stories
  • About Me
Edit Content
FabTrader_Logo_Main
Welcome to FabTrader Community!

Course Dashboard
Store Account
Cart
Algo Trading Course
Other Courses
About Me
Store
Stories

Get in touch with me

hello@fabtrader.in

blog banner background shape images
  • FabTrader
  • March 27, 2025

The Phoenix Bird – Swing Trading Strategy – Backtested!

  • 12 min read
  • 387 Views
Introduction

Stock markets often overreact, causing certain stocks to drop significantly in price. However, some of these stocks quickly recover, presenting a potential trading opportunity. The Phoenix Bird strategy is designed to identify such stocks that have hit bottom but show signs of resurgence. By applying a systematic approach to detect these stocks, we aim to capitalize on their potential rebound.

Strategy Overview

  • The Phoenix Bird strategy is a long-only swing trading strategy.
  • It uses daily candlestick data and is executed at the end of the trading day to find possible entries for the next day’s open.
  • The goal is to identify stocks that have recently dropped sharply but show recovery signs.

Entry Rules

A stock qualifies for entry if it meets the following conditions at the end of the trading day:

  1. Rate of Change (ROC): The percentage difference between today’s closing price and the closing price 14 days ago must be less than -20% (indicating a sharp drop). ROC indicator is available on Trading View as well.
  2. Momentum Improvement: The ROC today must be more positive than the ROC yesterday.
  3. Recovery Confirmation: The ROC today must also be more positive than the ROC three days ago.

If these conditions are met, the trade is entered at the next day’s market open.

Exit Rules

The trade will be exited under the following conditions:

  1. Target Price Hit: The target is 1x the Average True Range (ATR) value calculated at the time of entry.
  2. Stop Loss Hit: The stop loss is set at 2.5x the ATR value calculated at the time of entry.
  3. Time-Based Exit: If neither the target nor stop loss is hit, the trade will be automatically closed after 10 days.

Python Script to Back Test this strategy

Following is the python code to back test this strategy. Only change you may want to do this script is replace your own respective historic data function with the one given here.

"""
Strategy : Phoenix
Psychology : Sometimes stocks go down in price due to market over-reaction and recovers fast.
This strategy identifies such stocks that hit bottom but is showing signs of recovery.

Rules of strategy:
- This uses daily candles and is run at the end of day to identify potential entries for next day open.
- This is a long-only strategy and hence no shorts
- Strategy will be run after the market is closed for the day and the entry into stock is done the next day open if its satisfies the following conditions.
1. The Rate of change (which is the percentage difference in prices between today's close and close 14 days earlier) is less than -20
2. The Rate of change today is more positive than the rate of change the previous day
3. The Rate of change today is more positive than the rate of change 3 days earlier
- The Target is 1 times ATR range value. The ATR value is to be calculated at the time of entry and maintained at that level till the end of trade
- Stop loss is 2.5 times ATR range value as calculated at the time of entry
- The trade automatically closes if the trade is open for more than 10 days and both target and stoploss are not hit

Custom Modifications:
- Timelimit exit reduced to 10 days (from 20 days)
- Too few trades for -20 ROC limit. Changed it to -15.

"""

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import date
import seaborn as sns


class PhoenixStrategy:
    def __init__(self, tickers, start_date, end_date):
        self.tickers = tickers
        self.ticker = None
        self.start_date = start_date
        self.end_date = end_date
        self.data = None
        self.trades = []
        self.trade_stats = {}

    def fetch_data(self):
        #  Replace this function below with your own function that fetches historic data from your respective broker/resource
        self.data = Instruments.get_historical_data(self.ticker, self.start_date, self.end_date)
        # print(f"Downloaded {len(self.data)} rows of data for {self.ticker}")
        return self.data

    def prepare_data(self):
        # Calculate Rate of Change (ROC) for 14 days
        self.data['ROC_14'] = self.data['Close'].pct_change(14) * 100

        # Calculate ROC from 1 day ago and 3 days ago
        self.data['ROC_14_prev'] = self.data['ROC_14'].shift(1)
        self.data['ROC_14_prev3'] = self.data['ROC_14'].shift(3)

        # Calculate ATR (Average True Range) for setting targets and stop losses
        self.data['TR'] = np.maximum(
            self.data['High'] - self.data['Low'],
            np.maximum(
                abs(self.data['High'] - self.data['Close'].shift(1)),
                abs(self.data['Low'] - self.data['Close'].shift(1))
            )
        )
        self.data['ATR_14'] = self.data['TR'].rolling(window=14).mean()

        # Drop NaN values
        self.data = self.data.dropna()
        return self.data

    def backtest(self):
        # Initialize variables
        in_position = False
        entry_price = 0
        entry_date = None
        entry_atr = 0
        target = 0
        stop_loss = 0
        days_in_trade = 0

        for ticker in self.tickers:
            self.ticker = ticker
            self.fetch_data()
            self.prepare_data()

            # Loop through each day
            for i in range(1, len(self.data)):
                if i == len(self.data)-1:
                    break  # End of data. Avoid error while calculating next_date
                current_date = self.data.index[i]
                prev_date = self.data.index[i - 1]
                next_date = self.data.index[i + 1]

                # If we're in a position, check if any exit conditions are met
                if in_position:
                    days_in_trade += 1
                    high_price = self.data.loc[current_date, 'High']
                    low_price = self.data.loc[current_date, 'Low']
                    close_price = self.data.loc[current_date, 'Close']

                    # Check if target hit during the day
                    if high_price >= target:
                        self.record_trade(entry_date, current_date, entry_price, target, 'target', days_in_trade, entry_atr)
                        print("Target Hit", current_date)
                        in_position = False
                        days_in_trade = 0

                    # Check if stop loss hit during the day
                    elif low_price <= stop_loss:
                        self.record_trade(entry_date, current_date, entry_price, stop_loss, 'stop_loss', days_in_trade,
                                          entry_atr)
                        print("SL Hit", current_date)
                        in_position = False
                        days_in_trade = 0

                    # Check if 10-day limit reached
                    elif days_in_trade >= 10:
                        self.record_trade(entry_date, current_date, entry_price, close_price, 'time_limit', days_in_trade,
                                          entry_atr)
                        print("Time Limit Hit", current_date)
                        in_position = False
                        days_in_trade = 0

                # If not in position, check entry conditions for next day
                else:
                    # Check entry conditions
                    roc_14 = self.data.loc[prev_date, 'ROC_14']
                    roc_14_prev = self.data.loc[prev_date, 'ROC_14_prev']
                    roc_14_prev3 = self.data.loc[prev_date, 'ROC_14_prev3']

                    entry_condition = (
                            # roc_14 < -20 and  # ROC is less than -20%
                            roc_14_prev < -15 and  # ROC is less than -15%
                            roc_14 > roc_14_prev   # ROC today > ROC yesterday
                            and roc_14 > roc_14_prev3  # ROC today > ROC 3 days ago
                    )

                    if entry_condition:
                        # Enter position at next day's open
                        entry_price = self.data.loc[next_date, 'Open']
                        entry_date = next_date

                        entry_atr = self.data.loc[current_date, 'ATR_14']  # Use ATR from previous day (at time of decision)

                        # Set target and stop loss
                        target = entry_price + (entry_atr * 1.0)
                        stop_loss = entry_price - (entry_atr * 2.5)
                        print("Entry Triggered for ", self.ticker," Date ", entry_date, " Price ", entry_price, " Target ", target, " Stop Loss ", stop_loss)
                        in_position = True
                        days_in_trade = 1

        return self.trades

    def record_trade(self, entry_date, exit_date, entry_price, exit_price, exit_reason, days_held, atr):
        pnl = ((exit_price / entry_price) - 1) * 100  # Percentage gain/loss
        pnl_r = pnl / (atr / entry_price * 100)  # R multiple (profit in terms of ATR)

        trade = {
            'entry_date': entry_date,
            'exit_date': exit_date,
            'entry_price': entry_price,
            'exit_price': exit_price,
            'exit_reason': exit_reason,
            'days_held': days_held,
            'pnl_percent': pnl,
            'atr_at_entry': atr,
            'pnl_r': pnl_r
        }

        self.trades.append(trade)
        return trade

    def analyze_performance(self):
        if not self.trades:
            print("No trades were executed in the backtest period.")
            return {}

        # Convert trades to DataFrame for analysis
        trades_df = pd.DataFrame(self.trades)

        # Overall statistics
        total_trades = len(trades_df)
        winning_trades = len(trades_df[trades_df['pnl_percent'] > 0])
        losing_trades = len(trades_df[trades_df['pnl_percent'] <= 0])

        win_rate = winning_trades / total_trades if total_trades > 0 else 0

        avg_win = trades_df[trades_df['pnl_percent'] > 0]['pnl_percent'].mean() if winning_trades > 0 else 0
        avg_loss = trades_df[trades_df['pnl_percent'] <= 0]['pnl_percent'].mean() if losing_trades > 0 else 0

        profit_factor = abs(trades_df[trades_df['pnl_percent'] > 0]['pnl_percent'].sum() /
                            trades_df[trades_df['pnl_percent'] <= 0][
                                'pnl_percent'].sum()) if losing_trades > 0 else float('inf')

        # Total return calculation
        initial_capital = 100000  # Assuming $10,000 starting capital
        capital = initial_capital
        equity_curve = [initial_capital]

        for _, trade in trades_df.iterrows():
            trade_return = trade['pnl_percent'] / 100  # Convert percentage to decimal
            capital = capital * (1 + trade_return)
            equity_curve.append(capital)

        total_return_pct = ((capital - initial_capital) / initial_capital) * 100

        # Calculate drawdown
        peaks = pd.Series(equity_curve).cummax()
        drawdowns = (pd.Series(equity_curve) / peaks - 1) * 100
        max_drawdown = drawdowns.min()

        # Calculate Sharpe Ratio (assuming 252 trading days in a year)
        trade_returns = trades_df['pnl_percent'] / 100
        sharpe_ratio = np.sqrt(total_trades) * trade_returns.mean() / trade_returns.std() if len(
            trade_returns) > 1 else 0

        # Group trades by exit reason
        exit_reasons = trades_df.groupby('exit_reason').agg({
            'pnl_percent': ['count', 'mean', 'sum']
        })

        # Average trade duration
        avg_days_held = trades_df['days_held'].mean()

        # R-multiple statistics
        avg_r = trades_df['pnl_r'].mean()

        # Store all stats in a dictionary
        self.trade_stats = {
            'total_trades': total_trades,
            'winning_trades': winning_trades,
            'losing_trades': losing_trades,
            'win_rate': win_rate,
            'avg_win_pct': avg_win,
            'avg_loss_pct': avg_loss,
            'profit_factor': profit_factor,
            'total_return_pct': total_return_pct,
            'max_drawdown_pct': max_drawdown,
            'sharpe_ratio': sharpe_ratio,
            'avg_trade_duration': avg_days_held,
            'avg_r_multiple': avg_r,
            'exit_reasons': exit_reasons,
            'equity_curve': equity_curve
        }

        return self.trade_stats

    def display_results(self):
        if not self.trade_stats:
            print("No analysis available. Run analyze_performance() first.")
            return

        print("\n====== Phoenix Strategy Performance Report ======")
        # print(f"Symbol: {self.ticker}")
        print(f"Period: {self.start_date} to {self.end_date}")
        print(f"Total Trades: {self.trade_stats['total_trades']}")
        print(f"Win Rate: {self.trade_stats['win_rate']:.2%}")
        print(f"Average Winner: {self.trade_stats['avg_win_pct']:.2f}%")
        print(f"Average Loser: {self.trade_stats['avg_loss_pct']:.2f}%")
        print(f"Profit Factor: {self.trade_stats['profit_factor']:.2f}")
        print(f"Total Return: {self.trade_stats['total_return_pct']:.2f}%")
        print(f"Max Drawdown: {self.trade_stats['max_drawdown_pct']:.2f}%")
        print(f"Sharpe Ratio: {self.trade_stats['sharpe_ratio']:.2f}")
        print(f"Average Trade Duration: {self.trade_stats['avg_trade_duration']:.2f} days")
        print(f"Average R-Multiple: {self.trade_stats['avg_r_multiple']:.2f}")

        # Display exit reason statistics
        print("\nExit Reason Breakdown:")
        print(self.trade_stats['exit_reasons'])

        # Plot equity curve
        plt.figure(figsize=(12, 6))
        plt.plot(self.trade_stats['equity_curve'])
        plt.title('Equity Curve')
        plt.xlabel('Trade Number')
        plt.ylabel('Account Value ($)')
        plt.grid(True)
        plt.show()

        # Plot distribution of returns
        plt.figure(figsize=(12, 6))
        trades_df = pd.DataFrame(self.trades)
        sns.histplot(trades_df['pnl_percent'], kde=True)
        plt.title('Distribution of Trade Returns')
        plt.xlabel('Return (%)')
        plt.ylabel('Frequency')
        plt.grid(True)
        plt.show()

        # Plot monthly returns
        trades_df['month'] = pd.to_datetime(trades_df['entry_date']).dt.to_period('M')
        monthly_returns = trades_df.groupby('month')['pnl_percent'].sum()

        plt.figure(figsize=(12, 6))
        monthly_returns.plot(kind='bar')
        plt.title('Monthly Returns')
        plt.xlabel('Month')
        plt.ylabel('Return (%)')
        plt.grid(True)
        plt.xticks(rotation=45)
        plt.tight_layout()
        plt.show()

        # Plot drawdown
        equity_series = pd.Series(self.trade_stats['equity_curve'])
        peaks = equity_series.cummax()
        drawdowns = (equity_series / peaks - 1) * 100

        plt.figure(figsize=(12, 6))
        drawdowns.plot()
        plt.title('Drawdown')
        plt.xlabel('Trade Number')
        plt.ylabel('Drawdown (%)')
        plt.grid(True)
        plt.show()

    def run_full_backtest(self):
        self.backtest()
        self.analyze_performance()
        self.display_results()
        return self.trades, self.trade_stats


if __name__ == "__main__":
    pd.set_option("display.max_rows", None, "display.max_columns", None)
    tickers = [
        "INFY", "ITC", "ASIANPAINT", "BRITANNIA", "ICICIBANK", "HCLTECH", "TCS", "LTIM", "TITAN",
        "DRREDDY", "HINDUNILVR", "SBIN", "TATACONSUM", "HDFCBANK", "KOTAKBANK", "MARUTI", "BAJAJFINSV",
        "SBILIFE", "LT", "NESTLEIND","AXISBANK", "CIPLA", "BHARTIARTL", "HEROMOTOCO", "GRASIM", "DIVISLAB",
        "ADANIPORTS","TECHM","SUNPHARMA","EICHERMOT","INDUSINDBK","RELIANCE", "HDFCLIFE", "APOLLOHOSP",
        "M&M","SHRIRAMFIN", "BAJFINANCE","POWERGRID", "ADANIENT", "BAJAJ-AUTO","WIPRO","ULTRACEMCO", "TATAMOTORS",
        "NTPC", "COALINDIA", "ONGC", "HINDALCO", "BPCL", "JSWSTEEL", "TATASTEEL"
    ]

    # Back Test period
    start_date = date(2025, 1, 1)  # Modify as needed
    end_date = date(2025, 3, 31)  # Modify as needed

    strategy = PhoenixStrategy(tickers, start_date, end_date)
    trades, stats = strategy.run_full_backtest()

Back Test Results

With limited tests done, the strategy seems to be performing well. However, the number of trading opportunities seems low given that there would be very minimal occurrences of these scenario in market. With this free python back test code, you can run your own tests!

This is not a recommendation and use extreme caution and we advise you do your own research and back testing before using this strategy. This article is for educational purpose only and should not be construed as investment advice.

Key Takeaways

  • The Phoenix Bird strategy focuses on mean reversion, capitalizing on market overreactions.
  • By incorporating ATR-based targets and stop losses, risk is effectively managed.
  • The time-based exit ensures that trades don’t remain open indefinitely.
  • Backtest results suggest that this strategy has the potential to generate consistent swing trading opportunities.
Conclusion

The Phoenix Bird strategy is an effective way to identify stocks that have bottomed out and are poised for a rebound. By following a strict set of entry and exit rules, traders can systematically take advantage of these opportunities while maintaining proper risk management.

Would you like to explore this strategy further? Let’s discuss its real-world application and optimization in the comments!


Support this community : FabTrader.in is a one-person initiative dedicated to helping individuals on their F.I.R.E. journey. Running and maintaining this community takes time, effort, and resources. If you’ve found value in the content, consider making a donation to support this mission.

Donate

Disclaimer: The information provided in this article is for educational and informational purposes only and should not be construed as financial, investment, or legal advice. The content is based on publicly available information and personal opinions and may not be suitable for all investors. Investing involves risks, including the loss of principal. Always conduct your own research and consult a qualified financial advisor before making any investment decisions. The author and website assume no liability for any financial losses or decisions made based on the information presented.

FabTrader

Vivek is an algorithmic trader, Python programmer, and a passionate advocate of the F.I.R.E. (Financial Independence, Retire Early) movement. He achieved his financial independence at the age of 45 and is dedicated to helping others embark on their own journeys toward financial freedom.

Home
Store
Stories
Algo Trading Platform Using Python Course
About Me

©2024 Fabtrader.in - An unit of Rough Sketch Company. All Rights Reserved

Terms & Conditions
Privacy Policy