Skip to content

Algo Trading

The Phoenix Bird - Swing Trading Strategy - Backtested!

27 March 202511 min readAlgo Trading
FabTrader author portrait

FabTrader

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.

How I Backtested This

I have a full-fledged backtesting framework in Python, built to test trading strategies with precision.

This is the same framework I teach in my course: Backtesting Trading Strategies using AI and Python. If you want to learn how to build such backtesters, test strategies like this one, and validate them with real numbers, you can check out the course here: [Link to Course].

And if you’re someone who wants to go one step further – not just test strategies but actually deploy them in real markets in full auto mode – I also run a course on Building a Complete Algo Trading System in Python.

In that program, I cover how to:

  • Automate your strategies from end to end.
  • Run them on an AWS server 24/7.
  • Implement risk and money management.
  • Build a real-time monitoring dashboard.
  • Even manage trades from a Telegram bot.

Basically, it’s everything you need to go from a backtested strategy to a production-grade automated system. You can learn more about that here: [Link to Course].

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!

More from Algo Trading