Multiple-Criteria Technical Analysis of Blue Chips in Python

  • Blue chip stocks are the stocks of well-known, high-quality companies that are industry leaders.
  • The blue chip stocks’ attractive risk-reward profiles make them among the most popular for conservative investors. But even more risk-tolerant investors should consider buying blue chip stocks to diversify their portfolios better and provide stability during turbulent stock market conditions.
  • In portfolio management, we are interested in getting high returns while simultaneously reducing risks; however, the stocks that have the potential of bringing high returns typically carry high risk of losing money. 
  • Portfolio optimization balances risk and return by combining risky and safe investments in a ratio that matches the investor’s risk tolerance.
  • Failure to consider multiple attributes and criteria for portfolio evaluation is one of the most significant drawbacks of conventional technical analysis models.
  • In this post, we will overcome these drawbacks by combining several robust trading indicators and risk management algorithms to facilitate the trading decision-making.
  • The current focus is on the art of forecasting price movements through the study of chart patterns, trading signals, risk metrics, etc.
  • Our ultimate goal is to demonstrate that the proposed integrated approach can help optimize the blue-chip portfolios comprehensively and support traders to execute their trading strategies effectively.

Table of Contents

  1. AMZN Moving Averages
  2. AMZN Market Direction
  3. AMZN Feature Importance
  4. AAPL Market Capture Ratios
  5. TSLA Bollinger Bands
  6. TSLA RSI Trading Signals
  7. TSLA MACD Signal Line
  8. Portfolio Returns
  9. Stock Correlations
  10. Standard Deviations
  11. EWA
  12. $$$ ROI
  13. Sharpe Ratio
  14. Evaluation
  15. Summary
  16. Explore More
  17. Appendix: Supply-Demand Levels

AMZN Moving Averages

We begin with moving averages (MA) that help to filter out the noise and act as trend-following indicators. When the price level of a stock rises above MA, traders consider it as an indication to buy. And when the price falls below MA, traders assume it as a signal to sell.

Let’s set the working directory PATH

import os
os.chdir('PATH')     # Set working directory
os. getcwd()

and download the AMZN stock data using yfinance

import yfinance as yf
import pandas as pd

ticker = 'AMZN'  
start_date = '2007-01-01'
end_date = '2023-09-29'

stock_data = yf.download(ticker, start=start_date, end=end_date)

Let’s compute and plot MA 5-20-50-200

stock_data['MovingAverage_5'] = stock_data['Adj Close'].rolling(window=5).mean()
stock_data['MovingAverage_20'] = stock_data['Adj Close'].rolling(window=20).mean()
stock_data['MovingAverage_50'] = stock_data['Adj Close'].rolling(window=50).mean()
stock_data['MovingAverage_200'] = stock_data['Adj Close'].rolling(window=200).mean()
stock_data.dropna(inplace=True)
import numpy as np
import pandas as pd
from math import sqrt
import matplotlib.pyplot as plt
symbol=ticker
#series = stock_data['Adj Close']
#series.index = np.arange(series.shape[0])
plt.figure(figsize=(16, 10))
plt.rcParams.update({'font.size': 22})
plt.title(symbol)
plt.xlabel('Days')
plt.ylabel('Prices')
#plt.plot(stock_data['Adj Close'], label=symbol)
pts=stock_data['MovingAverage_5']
plt.plot(pts, label=f'MA5 {symbol}')
pts=stock_data['MovingAverage_20']
plt.plot(pts, label=f'MA20 {symbol}')
pts=stock_data['MovingAverage_50']
plt.plot(pts, label=f'MA50 {symbol}')
pts=stock_data['MovingAverage_200']
plt.plot(pts, label=f'MA200 {symbol}')
plt.legend()
plt.grid()
AMZN MA 5-20-50-200

AMZN Market Direction

Let’s compute the AMZN Daily Returns and Market Direction

def create_labels(data, threshold=0.0):
    data['MarketDirection'] = 0
    data.loc[data['DailyReturns'] <= -threshold, 'MarketDirection'] = 1

stock_data['DailyReturns'] = stock_data['Adj Close'].pct_change()

create_labels(stock_data, threshold=0.02)

The Daily Returns AMZN plot is given by

plt.figure(figsize=(16, 6))
pts=stock_data['DailyReturns']
plt.plot(pts, label=f'Daily Returns {symbol}')
plt.legend()
plt.grid()
Daily Returns AMZN

AMZN Feature Importance

Let’s implement the Random Forest Binary Classifier of Market Direction

from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split

X = stock_data[['MovingAverage_5', 'MovingAverage_20', 'MovingAverage_50', 'MovingAverage_200']]
y = stock_data['MarketDirection']

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
model = RandomForestClassifier(n_estimators=100, random_state=42)

model.fit(X_train, y_train)
from sklearn.metrics import accuracy_score, classification_report
import matplotlib.pyplot as plt

y_pred = model.predict(X_test)

accuracy = accuracy_score(y_test, y_pred)
print(f"Model Accuracy: {accuracy:.2f}")

classification_report_str = classification_report(y_test, y_pred)
print("Classification Report:")
print(classification_report_str)

feature_importances = model.feature_importances_
features = X.columns

plt.figure(figsize=(8, 6))
plt.bar(features, feature_importances)
plt.title('Feature Importances')
plt.xlabel('Features')
plt.ylabel('Importance')
plt.xticks(rotation=45)
plt.show()
Model Accuracy: 0.84
Classification Report:
              precision    recall  f1-score   support

           0       0.88      0.94      0.91       706
           1       0.19      0.09      0.12        97

    accuracy                           0.84       803
   macro avg       0.54      0.52      0.52       803
weighted avg       0.80      0.84      0.82       803
AMZN Random Forest feature importance factors

AAPL Market Capture Ratios

One popular tool used to assess a strategy’s behavior versus a benchmark is the market capture ratio, which helps us assess an investment’s ability to capture both upside and downside movements in the market. 

Let’s import the basic libraries

import pandas as pd
import numpy as np
import yfinance as yf

and introduce the following market_capture_ratio function

# %% Function

def market_capture_ratio(returns):
    """
    Function to calculate the upside and downside capture for a given set of returns.
    The function is set up so that the investment's returns are in the first column of the dataframe
    and the index returns are the second column.
    :param returns: pd.DataFrame of asset class returns
    :return: pd.DataFrame of market capture results
    """

    # initialize an empty dataframe to store the results
    df_mkt_capture = pd.DataFrame()

    # 1) Upside capture ratio
    # a) Isolate positive periods of the index
    up_market = returns[returns.iloc[:, -1] >= 0]

    # b) Geometrically link the returns
    up_linked_rets = ((1 + up_market).product(axis=0)) - 1

    # c) Calculate the ratio, multiply by 100 and round to 2 decimals to show in percent
    up_ratio = (up_linked_rets / up_linked_rets.iloc[-1] * 100).round(2)

    # 2) Downside capture ratio
    # a) Isolate negative periods of the index
    down_market = returns[returns.iloc[:, -1] < 0]

    # b) Geometrically link the returns
    down_linked_rets = ((1 + down_market).product(axis=0)) - 1

    # c) Calculate the ratio, multiply by 100 and round to 2 decimals to show in percent
    down_ratio = (down_linked_rets / down_linked_rets.iloc[-1] * 100).round(2)

    # 3) Combine to produce our final dataframe
    df_mkt_capture = pd.concat([up_ratio, down_ratio], axis=1)

    df_mkt_capture.columns = ['Upside Capture', 'Downside Capture']

    return df_mkt_capture

Let’s calculate the upside/downside capture ratios of AAPL vs SPY

# %% Retrieve the returns

# Specify the tickers to retrieve using yfinance
tickers = ['AAPL', 'SPY']
start_date = '2018-01-01'
end_date = '2023-09-29'

# Retrieve the historical data for the tickers
df_prices = yf.download(tickers, start=start_date, end=end_date)

# Keep only the adjusted close columns
df_prices = df_prices['Adj Close']

# Resample to month end and calculate the monthly percent change
df_rets_monthly = df_prices.resample('M').last().pct_change().dropna()

# Calculate the market capture ratios
df_mkt_capture = market_capture_ratio(df_rets_monthly)

print(df_mkt_capture)
         Upside Capture    Downside Capture
AAPL          306.46            101.94
SPY           100.00            100.00
  • The upside capture ratio of 306% means that the strategy would capture 306% of the positive performance of the benchmark or outperformed during the period. In other words, if the benchmark experienced the 10% gain, our investment would have produced the 30% gain.
  • The downside capture ratio of 102% means that our investment underperformed the benchmark during down periods. 
  • A strategy with ratios of over 100% would normally be indicative of strategies that are more aggressive than the underlying benchmark while ratios under 100% would be indicative of more conservative strategies.
  • Ideally, the goal is to create strategies that have upside capture ratios over 100% and downside ratios under 100%.
  • Read more here.

TSLA Bollinger Bands

  • Bollinger Bands, a technical indicator developed by John Bollinger, are used to measure a market’s volatility and identify “overbought” or “oversold” conditions.
  • Consider the TSLA example of using Bollinger Bands (BB) to compute trading signals
import pandas as pd
import yfinance as yf
import matplotlib.pyplot as plt

def generate_bollinger_bands(data):
    data['Middle_Band'] = data['Close'].rolling(window=20).mean()
    data['Upper_Band'] = data['Middle_Band'] + 2 * data['Close'].rolling(window=20).std()
    data['Lower_Band'] = data['Middle_Band'] - 2 * data['Close'].rolling(window=20).std()
    return data

if __name__ == "__main__":
    ticker_symbol = 'TSLA'  # Ticker symbol for Tesla Inc.
    start_date = '2023-01-01'  # Replace with the desired start date
    end_date = '2023-09-26'  # Replace with the desired end date

    # Fetch historical price data from Yahoo Finance
    data = yf.download(ticker_symbol, start=start_date, end=end_date)

    # Generate Bollinger Bands
    data = generate_bollinger_bands(data)

    # Plot Bollinger Bands and Closing Price
    plt.figure(figsize=(12, 6))
    plt.plot(data.index, data['Close'], label='Closing Price', color='orange')
    plt.plot(data.index, data['Upper_Band'], label='Upper Band', color='red')
    plt.plot(data.index, data['Middle_Band'], label='Middle Band', color='blue')
    plt.plot(data.index, data['Lower_Band'], label='Lower Band', color='green')

    plt.title(f'{ticker_symbol} Bollinger Bands')
    plt.xlabel('Date')
    plt.ylabel('Price')
    plt.legend(loc='lower right')
    plt.grid()
    plt.show()
TSLA Bollinger Bands

The most basic interpretation of BB is that the channels represent a measure of ‘highness’ and ‘lowness’:

  • The upper band shows a level that is statistically high or expensive
  • The lower band shows a level that is statistically low or cheap
  • The BB width is linked to the stock volatility
    • In a more volatile case, BB widen
    • In a less volatile case, the bands narrow

TSLA RSI Trading Signals

  • Momentum indicators are technical analysis tools used to determine the strength or weakness of a stock’s price. Momentum measures the rate of the rise or fall of stock prices. Common momentum indicators include the relative strength index (RSI) and moving average convergence divergence (MACD).
import pandas as pd
import numpy as np
import datetime as dt
import matplotlib.pyplot as plt

from pandas_datareader import data as pdr
import yfinance as yf
yf.pdr_override()

# YFinance API Pull 

ticker = 'TSLA'

now = dt.datetime.now()
startyear = 2023
startmonth = 1 
startday = 2
start = dt.datetime(startyear, startmonth, startday)

df = pdr.get_data_yahoo(ticker, start = "2023-01-01", end = "2023-09-29")

df.tail()
TSLA stock input table
# 14_Day RSI
df['Up Move'] = np.nan
df['Down Move'] = np.nan
df['Average Up'] = np.nan
df['Average Down'] = np.nan
# Relative Strength
df['RS'] = np.nan
# Relative Strength Index
df['RSI'] = np.nan

#Calculate Up Move & Down Move
for x in range(1, len(df)):
    df['Up Move'][x] = 0
    df['Down Move'][x] = 0
    
    if df['Adj Close'][x] > df['Adj Close'][x-1]:
        df['Up Move'][x] = df['Adj Close'][x] - df['Adj Close'][x-1]
        
    if df['Adj Close'][x] < df['Adj Close'][x-1]:
        df['Down Move'][x] = abs(df['Adj Close'][x] - df['Adj Close'][x-1])  
        
#Calculate initial Average Up & Down, RS and RSI
df['Average Up'][14] = df['Up Move'][1:15].mean()
df['Average Down'][14] = df['Down Move'][1:15].mean()
df['RS'][14] = df['Average Up'][14] / df['Average Down'][14]
df['RSI'][14] = 100 - (100/(1+df['RS'][14]))

#Calculate rest of Average Up, Average Down, RS, RSI
for x in range(15, len(df)):
    df['Average Up'][x] = (df['Average Up'][x-1]*13+df['Up Move'][x])/14
    df['Average Down'][x] = (df['Average Down'][x-1]*13+df['Down Move'][x])/14
    df['RS'][x] = df['Average Up'][x] / df['Average Down'][x]
    df['RSI'][x] = 100 - (100/(1+df['RS'][x]))
    
print(df)
               Open        High         Low       Close   Adj Close  \
Date                                                                     
2023-01-03  118.470001  118.800003  104.639999  108.099998  108.099998   
2023-01-04  109.110001  114.589996  107.519997  113.639999  113.639999   
2023-01-05  110.510002  111.750000  107.160004  110.339996  110.339996   
2023-01-06  103.000000  114.389999  101.809998  113.059998  113.059998   
2023-01-09  118.959999  123.519997  117.110001  119.769997  119.769997   
...                ...         ...         ...         ...         ...   
2023-09-22  257.399994  257.790009  244.479996  244.880005  244.880005   
2023-09-25  243.380005  247.100006  238.309998  246.990005  246.990005   
2023-09-26  242.979996  249.550003  241.660004  244.119995  244.119995   
2023-09-27  244.259995  245.330002  234.580002  240.500000  240.500000   
2023-09-28  240.020004  247.550003  238.649994  246.380005  246.380005   

               Volume   Up Move  Down Move  Average Up  Average Down  \
Date                                                                   
2023-01-03  231402800       NaN        NaN         NaN           NaN   
2023-01-04  180389000  5.540001   0.000000         NaN           NaN   
2023-01-05  157986300  0.000000   3.300003         NaN           NaN   
2023-01-06  220911100  2.720001   0.000000         NaN           NaN   
2023-01-09  190284000  6.709999   0.000000         NaN           NaN   
...               ...       ...        ...         ...           ...   
2023-09-22  127024300  0.000000  10.819992    2.771808      3.635013   
2023-09-25  104636600  2.110001   0.000000    2.724536      3.375369   
2023-09-26  101993600  0.000000   2.870010    2.529926      3.339272   
2023-09-27  136597200  0.000000   3.619995    2.349217      3.359323   
2023-09-28  116811300  5.880005   0.000000    2.601416      3.119372   

                  RS        RSI  
Date                             
2023-01-03       NaN        NaN  
2023-01-04       NaN        NaN  
2023-01-05       NaN        NaN  
2023-01-06       NaN        NaN  
2023-01-09       NaN        NaN  
...              ...        ...  
2023-09-22  0.762530  43.263389  
2023-09-25  0.807182  44.665218  
2023-09-26  0.757628  43.105140  
2023-09-27  0.699313  41.152672  
2023-09-28  0.833955  45.473039  

[186 rows x 12 columns]
## Chart the stock price and RSI
fig, axs = plt.subplots(2, sharex=True, figsize=(13,9))
fig.suptitle('TSLA Price (top) - 14 day RSI (bottom)')

axs[0].plot(df['Adj Close'])
axs[1].plot(df['RSI'])
axs[0].grid()
axs[1].grid()
## Calculate the buy & sell signals
## Initialize columns

df['Long Tomorrow'] = np.nan
df['Buy Signal'] = np.nan
df['Sell Signal'] = np.nan
df['Buy RSI'] = np.nan
df['Sell RSI'] = np.nan
df['Strategy'] = np.nan

#Calculate the buy & sell signals
for x in range(15, len(df)):
    
    #calculate "Long Tomorrow" column
    if ((df['RSI'][x] <= 40) & (df['RSI'][x-1]>40) ):
        df['Long Tomorrow'][x] = True
    elif ((df['Long Tomorrow'][x-1] == True) & (df['RSI'][x] <= 70)):
        df['Long Tomorrow'][x] = True
    else:
        df['Long Tomorrow'][x] = False
        
    #calculate "Buy Signal" column
    if ((df['Long Tomorrow'][x] == True) & (df['Long Tomorrow'][x-1] == False)):
        df['Buy Signal'][x] = df['Adj Close'][x]
        df['Buy RSI'][x] = df['RSI'][x]
        
    #calculate "Sell Signal" column
    if ((df['Long Tomorrow'][x] == False) & (df['Long Tomorrow'][x-1] == True)):
        df['Sell Signal'][x] = df['Adj Close'][x]
        df['Sell RSI'][x] = df['RSI'][x]
        
#calculate strategy performance
df['Strategy'][15] = df['Adj Close'][15]

for x in range(16, len(df)):
    if df['Long Tomorrow'][x-1] == True:
        df['Strategy'][x] = df['Strategy'][x-1]* (df['Adj Close'][x] / df['Adj Close'][x-1])
    else:
        df['Strategy'][x] = df['Strategy'][x-1]
## Chart the buy/sell signals

#plt.style.use('_classic_test')
fig, axs = plt.subplots(2, sharex=True, figsize=(13,9))
fig.suptitle('Stock Price (top) & 14 day RSI (bottom)')

## Chart the stock close price & buy/sell signals

axs[0].scatter(df.index, df['Buy Signal'],  color = 'green',  marker = '^', alpha = 1)
axs[0].scatter(df.index, df['Sell Signal'],  color = 'red',  marker = 'v', alpha = 1)
axs[0].plot(df['Adj Close'], alpha = 0.8)
axs[0].grid()

## Chart RSI & buy/sell signals

axs[1].scatter(df.index, df['Buy RSI'],  color = 'green', marker = '^', alpha = 1)
axs[1].scatter(df.index, df['Sell RSI'],  color = 'red', marker = 'v', alpha = 1)
axs[1].plot(df['RSI'], alpha = 0.8)
axs[1].grid()
TSLA price vs 14 day RSI
TSLA RSI trading signals

Low RSI levels, below 40, generate buy signals and indicate an oversold or undervalued condition. High RSI levels, above 70, generate sell signals and suggest that a security is overbought or overvalued.

TSLA MACD Signal Line

## Calculate the MACD and Signal Line indicators
## Calculate the Short Term Exponential Moving Average

ShortEMA = df.Close.ewm(span=12, adjust=False).mean() 

## Calculate the Long Term Exponential Moving Average

LongEMA = df.Close.ewm(span=26, adjust=False).mean() 

## Calculate the Moving Average Convergence/Divergence (MACD)

MACD = ShortEMA - LongEMA

## Calculate the signal line
signal = MACD.ewm(span=9, adjust=False).mean()
#Plot the chart

plt.figure(figsize=(14,8)) 
plt.style.use('classic')

plt.plot(df.index, MACD, label='MACD', color = 'blue')
plt.plot(df.index, signal, label='Signal Line', color='red')
plt.xticks(rotation=45)
plt.legend(loc='lower left')
plt.show()
TSLA MACD

In the above plot, signal line crossovers are the most common MACD signals. A bullish crossover occurs when the MACD turns up and crosses above the signal line. A bearish crossover occurs when the MACD turns down and crosses below the signal line. Crossovers can last a few days or a few weeks, depending on the strength of the move.

Portfolio Returns

Let’s create and optimize the blue-chip portfolio using the trading dashboard.

  • Input data
# Import libraries and dependencies
import numpy as np
import pandas as pd
import yfinance as yf
## AAPL DATA

cmg = yf.Ticker("AAPL")

cmg_historical = cmg.history(start="2023-1-1", end="2023-9-29", interval="1d")
cmg_df = cmg_historical.drop(columns=['Open', 'High', 'Low', 'Dividends', 'Stock Splits', 'Volume'])
cmg_df.rename(columns= {'Close':'AAPL'}, inplace=True)
cmg_daily = cmg_df.pct_change()
## JPM DATA

shop = yf.Ticker("JPM")

shop_historical = shop.history(start="2023-1-1", end="2023-9-29", interval="1d")
shop_df = shop_historical.drop(columns=['Open', 'High', 'Low', 'Dividends', 'Stock Splits', 'Volume'])
shop_df.rename(columns= {'Close':'JPM'}, inplace=True)
shop_daily = shop_df.pct_change()
## TSLA DATA

tsla= yf.Ticker("TSLA")

tsla_historical = tsla.history(start="2023-1-1", end="2023-9-26", interval="1d")
tsla_df = tsla_historical.drop(columns=['Open', 'High', 'Low', 'Dividends', 'Stock Splits', 'Volume'])
tsla_df.rename(columns= {'Close':'TSLA'}, inplace=True)
tsla_daily = tsla_df.pct_change()
## PG DATA

panw = yf.Ticker("PG")

panw_historical = panw.history(start="2023-1-1", end="2023-9-29", interval="1d")
panw_df = panw_historical.drop(columns=['Open', 'High', 'Low', 'Dividends', 'Stock Splits', 'Volume'])
panw_df.rename(columns= {'Close':'PG'}, inplace=True)
panw_daily = panw_df.pct_change()
## AMAZON DATA

amzn = yf.Ticker("AMZN")

amzn_historical = amzn.history(start="2023-1-1", end="2023-9-29", interval="1d")
amzn_df = amzn_historical.drop(columns=['Open', 'High', 'Low', 'Dividends', 'Stock Splits', 'Volume'])
amzn_df.rename(columns= {'Close':'AMZN'}, inplace=True)
amzn_daily = amzn_df.pct_change()
## META DATA

fb = yf.Ticker("META")

fb_historical = fb.history(start="2023-1-1", end="2023-9-29", interval="1d")
fb_df = fb_historical.drop(columns=['Open', 'High', 'Low', 'Dividends', 'Stock Splits', 'Volume'])
fb_df.rename(columns= {'Close':'META'}, inplace=True)
fb_daily = fb_df.pct_change()
# Concatenate joining tickers into one DataFrame

portfolio_df = pd.concat([fb_daily, amzn_daily, tsla_daily, panw_daily, shop_daily, cmg_daily], axis="columns", join="inner")
daily_portfolio = portfolio_df
daily_portfolio.dropna()
Portfolio input table
  • Daily returns
plt.rcParams.update({'font.size': 22})
daily_portfolio.plot(figsize=(20, 10), title="Daily Returns",lw=4)
Portfolio daily returns
  • Cumulative returns
cumulative_returns = (1 + daily_portfolio).cumprod()
cumulative_returns.plot(figsize=(20, 10), title="Cumulative Returns",lw=4)
Portfolio cumulative returns

The figure compares the cumulative returns of investing $1 in January 2023 until 2023-09-25 for our 6 stocks.

Stock Correlations

Correlation is closely tied to diversification, the concept that certain types of risk can be mitigated by investing in assets that are not correlated.

# Construct a correlation table
corr_df = daily_portfolio.corr()
corr_df.style.background_gradient(cmap="summer")
Portfolio correlation table

This matrix tells you how strongly each asset in a portfolio is related to the others.

Standard Deviations

Standard deviation (STD) is the most common way to measure market volatility, and traders can use BB to analyze STD.  In most cases, when the price of an asset is trending upwards, the standard deviation is usually relatively low.

# Daily Standard Deviations
# Calculate the standard deviation for each portfolio. 
daily_portfolio.std()
META    0.027521
AMZN    0.021600
TSLA    0.034879
PG      0.009199
JPM     0.014168
AAPL    0.013388
dtype: float64
# Calculate the annualized standard deviation (252 trading days)
daily_portfolio.std() * np.sqrt(252)
META    0.436883
AMZN    0.342885
TSLA    0.553688
PG      0.146036
JPM     0.224904
AAPL    0.212530
dtype: float64
# Calculate and plot the rolling standard deviation for
# the blue-chip portfolio using a 21 trading day window
daily_portfolio.rolling(window=21).std().plot(figsize=(20, 10), title="21 Day Rolling Standard Deviation",lw=4)
21 day rolling standard deviation of blue-chip stocks

EWA

An exponentially weighted moving average reacts more significantly to recent price changes than a simple moving average (SMA), which applies an equal weight to all observations in the period. This is a technical chart indicator that tracks the price of an investment (like a stock or commodity) over time. 

# Calculate a rolling window using the exponentially weighted moving average. 
daily_portfolio.ewm(halflife=21).std().plot(figsize=(20, 10), title="Exponentially Weighted Average",lw=4)
Exponentially weighted average of blue-chip stocks

$$$ ROI

# Plot returns of the portfolio in terms of money
initial_investment = 10000
cumulative_profit = initial_investment * cumulative_returns
cumulative_profit.plot(figsize=(20, 10),lw=4)
Returns of the portfolio in terms of money

The above plot provides a glimpse of the investment’s prior performance and helps determine if a particular investment has been profitable in 2023.

Sharpe Ratio

The Sharpe ratio compares the return of an investment with its risk. A higher Sharpe ratio is better when comparing similar portfolios. Usually, any Sharpe ratio greater than 1.0 is considered acceptable to good by investors. A ratio higher than 2.0 is rated as very good. A ratio of 3.0 or higher is considered excellent. A ratio under 1.0 is considered sub-optimal.

# Calculate annualized Sharpe Ratios
sharpe_ratios = (daily_portfolio.mean() * 252) / (daily_portfolio.std() * np.sqrt(252))
sharpe_ratios
META    3.000124
AMZN    1.888006
TSLA    2.343991
PG      0.193469
JPM     0.743038
AAPL    2.363905
dtype: float64
# Visualize the sharpe ratios as a bar plot
sharpe_ratios.plot(figsize=(15, 8),kind="bar", title="Sharpe Ratios", color= 'orange')
Stock Sharpe Ratios

Evaluation

# Prepare DataFrame for metrics
metrics = [
    'META Annual Return', 'AMZN Annual Return', 'TSLA Annual Return','PG Annual Return','JPM Annual Return', 'AAPL Annual Return',
    'META Annual Volatility','AMZN Annual Volatility','TSLA Annual Volatility','PG Annual Volatility','JPM Annual Volatility','AAPL Annual Volatility',
    'META Sharpe Ratio','AMZN Sharpe Ratio','TSLA Sharpe Ratio','PG Sharpe Ratio','JPM Sharpe Ratio','AAPL Sharpe Ratio']

columns = ['Backtest']

# Initialize the DataFrame with index set to evaluation metrics and column as `Backtest` (just like PyFolio)
portfolio_evaluation_df = pd.DataFrame(index=metrics, columns=columns)
# Calculate annualized return
portfolio_evaluation_df.loc['META Annual Return'] = daily_portfolio['META'].std() * np.sqrt(252)
portfolio_evaluation_df.loc['AMZN Annual Return'] = daily_portfolio['AMZN'].std() * np.sqrt(252)
portfolio_evaluation_df.loc['TSLA Annual Return'] = daily_portfolio['TSLA'].std() * np.sqrt(252)
portfolio_evaluation_df.loc['PG Annual Return'] = daily_portfolio['PG'].std() * np.sqrt(252)
portfolio_evaluation_df.loc['JPM Annual Return'] = daily_portfolio['JPM'].std() * np.sqrt(252)
portfolio_evaluation_df.loc['AAPL Annual Return'] = daily_portfolio['AAPL'].std() * np.sqrt(252)

# Calculate annual volatility
portfolio_evaluation_df.loc['META Annual Volatility'] = daily_portfolio['META'].std() * np.sqrt(252)
portfolio_evaluation_df.loc['AMZN Annual Volatility'] = daily_portfolio['AMZN'].std() * np.sqrt(252)
portfolio_evaluation_df.loc['TSLA Annual Volatility'] = daily_portfolio['TSLA'].std() * np.sqrt(252)
portfolio_evaluation_df.loc['PG Annual Volatility'] = daily_portfolio['PG'].std() * np.sqrt(252)
portfolio_evaluation_df.loc['JPM Annual Volatility'] = daily_portfolio['JPM'].std() * np.sqrt(252)
portfolio_evaluation_df.loc['AAPL Annual Volatility'] = daily_portfolio['AAPL'].std() * np.sqrt(252)

# Calculate Sharpe Ratio
portfolio_evaluation_df.loc['META Sharpe Ratio'] = sharpe_ratios['META']
portfolio_evaluation_df.loc['AMZN Sharpe Ratio'] = sharpe_ratios['AMZN']
portfolio_evaluation_df.loc['TSLA Sharpe Ratio'] = sharpe_ratios['TSLA']
portfolio_evaluation_df.loc['PG Sharpe Ratio'] = sharpe_ratios['PG']
portfolio_evaluation_df.loc['JPM Sharpe Ratio'] = sharpe_ratios['JPM']
portfolio_evaluation_df.loc['AAPL Sharpe Ratio'] = sharpe_ratios['AAPL']


portfolio_evaluation_df.head(18)
Portfolio backtest table
print (portfolio_evaluation_df)
                     index  Backtest
0       META Annual Return  0.436883
1       AMZN Annual Return  0.342885
2       TSLA Annual Return  0.553688
3         PG Annual Return  0.146036
4        JPM Annual Return  0.224904
5       AAPL Annual Return   0.21253
6   META Annual Volatility  0.436883
7   AMZN Annual Volatility  0.342885
8   TSLA Annual Volatility  0.553688
9     PG Annual Volatility  0.146036
10   JPM Annual Volatility  0.224904
11  AAPL Annual Volatility   0.21253
12       META Sharpe Ratio  3.000124
13       AMZN Sharpe Ratio  1.888006
14       TSLA Sharpe Ratio  2.343991
15         PG Sharpe Ratio  0.193469
16        JPM Sharpe Ratio  0.743038
17       AAPL Sharpe Ratio  2.363905
plt.figure(figsize=(16,8)) 
plt.rcParams.update({'font.size': 16})
plt.bar(portfolio_evaluation_df['index'],portfolio_evaluation_df['Backtest'])
plt.xticks(rotation=90)
Portfolio backtest bar plot

This plot shows the following:

  • max/min Return = TSLA/PG
  • max/min Volatility = TSLA/PG
  • max/min Sharpe Ratio = META/PG

Summary

  • In this post, we have tested the integrated multi-attribute technical analysis to optimize blue-chip stock portfolios.
  • We have investigated trend reversals of top blue chips (META, AMZN, TSLA, PG, JPM, and AAPL) by comparing various trading indicators (MA, EWA, MACD, RSI, and BB) and stock returns vs volatility measures.
  • We have chosen the optimal set of MA using the Random Forest dominance factors.
  • As an example, we have assessed the AAPL strategy behavior versus SPY using the market capture ratio.
  • We have examined the portfolio correlation matrix closely tied to diversification.
  • We have assessed the risk-adjusted performance of our portfolio in terms of the Sharpe ratio.
  • In Appendix, we have also examine the support and resistance levels of our portfolio based upon the law of supply and demand.
  • Our final stock evaluation metrics are Annual Return, Annual Volatility, and Annual Sharpe Ratio.

Explore More

Appendix: Supply-Demand Levels

  • The common technical analysis is focused on past price actions, usually using candlestick charts, to identify levels where a trend reversal occurred.
  • Instead, we will examine the support and resistance levels of our portfolio based upon the law of supply and demand. Following recent studies, we proposes using volatility for forecasting supply and demand zones in a form that isn’t subjective to personal interpretation of a price chart.
  • Importing key libraries
# Importing Libraries

# Data Handling
import pandas as pd
import numpy as np

# Financial Data Analysis
import yfinance as yf

# Data Visualization
import plotly.express as px
import plotly.graph_objs as go
import plotly.subplots as sp
from plotly.subplots import make_subplots
import plotly.figure_factory as ff
import plotly.io as pio
from IPython.display import display
from plotly.offline import init_notebook_mode

# Statistics & Mathematics
import scipy.stats as stats
import statsmodels as sm
from scipy.stats import shapiro, skew
import math

# Hiding warnings 
import warnings
warnings.filterwarnings("ignore")
  • Handling input data
def load_and_preprocess(ticker):
    '''
    This function takes in a ticker symbol, which is used to 
    retrieve historical data from Yahoo Finance.
    The attributes 'Returns', and the Adjusted Low, High, and Open 
    are created.
    NaNs are filled with 0s
    '''
    
    df = yf.download(ticker)
    df['Returns'] = df['Adj Close'].pct_change(1)
    df['Adj Low'] = df['Low'] - (df['Close'] - df['Adj Close'])
    df['Adj High'] = df['High'] - (df['Close'] - df['Adj Close'])
    df['Adj Open'] = df['Open'] - (df['Close'] - df['Adj Close'])
    df = df.fillna(0)
    return df
  • Selecting AAPL as an example
ticker = 'AAPL'

df = load_and_preprocess(ticker) # Loading and Transforming Dataframe
df.tail()
AAPL input data table
T = 20 # Time period for computing the standard deviation
df['Annualized_Vol'] = np.round(df['Returns'].rolling(T).std() * np.sqrt(252), 2)
# Creating a line plot to visualize historical volatility
lineplot = go.Scatter(
    x=df.index,
    y=df['Annualized_Vol'] * 100, # Annualized Volatility in %
    mode='lines',
    line=dict(color='darkblue', width=2.5),
    name = 'Annualized Volatility (%)')

layout = go.Layout(
    title={'text': f'<b>Annualized Volatility for {ticker} <br><i><sub></sub></i></b>',
           'x': 0.035, 'xanchor': 'left'},
            yaxis = dict(title = '<b>Annualized Volatility (%) </b>'),
            xaxis = dict(title = '<b>Date</b>'),
    template='seaborn',
    height = 450, width = 1000,
    showlegend=True,
    plot_bgcolor = '#F6F5F5',
    paper_bgcolor = '#F6F5F5',
    xaxis_rangeslider_visible=False)

fig = go.Figure(data=[lineplot], layout=layout)

fig.show()
Annualized volatility for AAPL

It is possible to detect in the plot above periods of high volatility where the stock experienced its highest points of fluctuation. 

# Yearly forecasting
reference_year = "2022" # Forecasting levels for 2023

High_Band_1std =  df.loc[reference_year]["Annualized_Vol"][-1]*df.loc[reference_year]["Adj Close"][-1] + df.loc[reference_year]["Adj Close"][-1]
Low_Band_1std =  df.loc[reference_year]["Adj Close"][-1] - df.loc[reference_year]["Annualized_Vol"][-1]*df.loc[reference_year]["Adj Close"][-1]

High_Band_2std =  2*df.loc[reference_year]["Annualized_Vol"][-1]*df.loc[reference_year]["Adj Close"][-1] + df.loc[reference_year]["Adj Close"][-1]
Low_Band_2std =  df.loc[reference_year]["Adj Close"][-1] - 2*df.loc[reference_year]["Annualized_Vol"][-1]*df.loc[reference_year]["Adj Close"][-1]

High_Band_3std =  3*df.loc[reference_year]["Annualized_Vol"][-1]*df.loc[reference_year]["Adj Close"][-1] + df.loc[reference_year]["Adj Close"][-1]
Low_Band_3std =  df.loc[reference_year]["Adj Close"][-1] - 3*df.loc[reference_year]["Annualized_Vol"][-1]*df.loc[reference_year]["Adj Close"][-1]
rint(f'\nVolatility-Based Supply and Demand Levels for {ticker} in {int(reference_year) + 1}\n')
print(f'Supply Level 3σ: {High_Band_3std.round(2)}\n')
print(f'Supply Level 2σ: {High_Band_2std.round(2)}\n')
print(f'Supply Level 1σ: {High_Band_1std.round(2)}\n')
print('-' * 65, '\n')
print(f'Demand Level 1σ: {Low_Band_1std.round(2)}\n')
print(f'Demand Level 2σ: {Low_Band_2std.round(2)}\n')
print(f'Demand Level 3σ: {Low_Band_3std.round(2)}\n')
Volatility-Based Supply and Demand Levels for AAPL in 2023

Supply Level 3σ: 241.94

Supply Level 2σ: 204.42

Supply Level 1σ: 166.9

----------------------------------------------------------------- 

Demand Level 1σ: 91.86

Demand Level 2σ: 54.34

Demand Level 3σ: 16.82
# Candlestick chart
candlestick = go.Candlestick(x = df.loc[str(int(reference_year) + 1)].index,
                             open = df.loc[str(int(reference_year) + 1)]['Adj Open'],
                             high = df.loc[str(int(reference_year) + 1)]['Adj High'],
                             low = df.loc[str(int(reference_year) + 1)]['Adj Low'],
                             close = df.loc[str(int(reference_year) + 1)]['Adj Close'],
                             increasing = dict(line=dict(color = 'red')),
                             decreasing = dict(line=dict(color = 'black')),
                             name = 'Candlesticks')


# Defining layout
layout = go.Layout(title = {'text': '<b>AAPL Volatility-Based Supply and Demand Levels<br><sup>&nbsp;&nbsp;&nbsp;&nbsp;<i>Annualized Forecast</i></sup></b>',
                            'x': .035, 'xanchor': 'left'},
                   yaxis = dict(title = '<b>Price (USD)</b>',
                               tickfont=dict(size=16)),
                   xaxis = dict(title = '<b>Date</b>'),
                   template = 'seaborn',
                   plot_bgcolor = '#F6F5F5',
                   paper_bgcolor = '#F6F5F5',
                   height = 850, width = 1000,
                   showlegend=False,
                   xaxis_rangeslider_visible = False)

# Defining figure
fig = go.Figure(data = [candlestick], layout = layout)

# Removing empty spaces (non-trading days)
dt_all = pd.date_range(start = df.index[0],
                       end = df.index[-1],
                       freq = "D")
dt_all_py = [d.to_pydatetime() for d in dt_all]
dt_obs_py = [d.to_pydatetime() for d in df.index]
dt_breaks = [d for d in dt_all_py if d not in dt_obs_py]

fig.update_xaxes(
    rangebreaks = [dict(values = dt_breaks)]
)

# Adding Supply and Demans Lines

# 1σ
fig.add_hline(y = High_Band_1std, line_width = 2, line_dash = "dot", line_color = "green")
fig.add_hline(y = Low_Band_1std, line_width = 2, line_dash = "dot", line_color = "red")

# 2σ
fig.add_hline(y = High_Band_2std, line_width = 4, line_dash = "dash", line_color = "green")
fig.add_hline(y = Low_Band_2std, line_width = 4, line_dash = "dash", line_color = "red")

# 3σ
fig.add_hline(y = High_Band_3std, line_width = 6, line_dash = "dashdot", line_color = "green")
fig.add_hline(y = Low_Band_3std, line_width = 6, line_dash = "dashdot", line_color = "red")

# Showing plot
fig.show()

AAPL volatility-based supply and demand levels

The output cell above gives us the specific supply and demand levels forecasted for the stock in 2023.

In the above plot, dot lines represent one standard deviation, 1σ (68.7% probability); dash lines represent two standard deviations, 2σ (95.4% probability); dash and dot lines represent three standard deviations, 3σ (99.7% probability).

Observe that the stock closed slightly higher than the 1σ supply band and below the 2σ supply band.

  • JPM
ticker = 'JPM'
Annualized volatility for JPM
Volatility-Based Supply and Demand Levels for JPM in 2023

Supply Level 3σ: 202.02

Supply Level 2σ: 178.4

Supply Level 1σ: 154.79

----------------------------------------------------------------- 

Demand Level 1σ: 107.57

Demand Level 2σ: 83.95

Demand Level 3σ: 60.34
JPM volatility-based supply and demand levels
  • PG
ticker = 'PG'
Annualized volatility for PG
Volatility-Based Supply and Demand Levels for PG in 2023

Supply Level 3σ: 197.82

Supply Level 2σ: 181.46

Supply Level 1σ: 165.1

----------------------------------------------------------------- 

Demand Level 1σ: 132.38

Demand Level 2σ: 116.02

Demand Level 3σ: 99.66
PG volatility-based supply and demand levels
  • TSLA
ticker = 'TSLA'
Annualized volatility for TSLA
Volatility-Based Supply and Demand Levels for TSLA in 2023

Supply Level 3σ: 396.64

Supply Level 2σ: 305.49

Supply Level 1σ: 214.33

----------------------------------------------------------------- 

Demand Level 1σ: 32.03

Demand Level 2σ: -59.13

Demand Level 3σ: -150.28
TSLA volatility-based supply and demand levels
  • AMZN
ticker = 'AMZN'
Annualized volatility for AMZN
Volatility-Based Supply and Demand Levels for AMZN in 2023

Supply Level 3σ: 169.68

Supply Level 2σ: 141.12

Supply Level 1σ: 112.56

----------------------------------------------------------------- 

Demand Level 1σ: 55.44

Demand Level 2σ: 26.88

Demand Level 3σ: -1.68
AMZN volatility-based supply and demand levels
  • META
ticker = 'META'
Volatility-Based Supply and Demand Levels for META in 2023

Supply Level 3σ: 286.41

Supply Level 2σ: 231.05

Supply Level 1σ: 175.7

----------------------------------------------------------------- 

Demand Level 1σ: 64.98

Demand Level 2σ: 9.63

Demand Level 3σ: -45.73
META volatility-based supply and demand levels

Go back

Your message has been sent

Warning

One-Time
Monthly
Yearly

Make a one-time donation

Make a monthly donation

Make a yearly donation

Choose an amount

€5.00
€15.00
€100.00
€5.00
€15.00
€100.00
€5.00
€15.00
€100.00

Or enter a custom amount


Your contribution is appreciated.

Your contribution is appreciated.

Your contribution is appreciated.

DonateDonate monthlyDonate yearly

Discover more from Our Blogs

Subscribe to get the latest posts sent to your email.

Leave a comment

Discover more from Our Blogs

Subscribe now to keep reading and get access to the full archive.

Continue reading