Foundations of Financial Analysis: From Returns to Risk

No of Post Views:

67 hits

Welcome to the fascinating world of financial data analysis. At the heart of every investment decision, from a speculative cryptocurrency purchase to a conservative government bond, lie two fundamental questions: “What is my potential return?” and “What is the risk involved?” Return is the ultimate measure of performance, but it’s only half of the story. The other, equally important half, is risk. An investment is a journey, and while the return tells you how high you’ve climbed, the risk describes the uncertainty of the path.

In this guide, we’ll embark on a practical journey to understand both sides of the investment equation. We will start with the basics of analyzing prices and calculating returns, and then build on that foundation to measure and interpret financial risk using various volatility metrics.

Analyzing Prices and Calculating Returns

Why Price Isn’t the Whole Picture

It’s tempting to look at the price of a stock and judge its value. A stock priced at $500 seems more “expensive” than one at $50. However, this is a common and often costly misconception. Price is simply a snapshot in time; it tells you the cost of entry, but very little about an investment’s potential or past performance. A $50 stock that doubles in value to $100 has provided a far better return (100%) than a $500 stock that inches up to $550 (10%).

To make meaningful comparisons, we need a scale-free measure. This is where returns come in. Returns tell you the percentage gain or loss, allowing for a true apples-to-apples comparison between assets of wildly different prices.

Let’s start by pulling some data into Python. We’ll look at two very different stocks, Amazon (AMZN) and Ford (F), along with Bitcoin (BTC-USD), over a five-year period.

import datetime
import matplotlib.pyplot as plt
import numpy as np
import yfinance as yfin

# Define the date range for the last 5 years
start = datetime.date(2016, 11, 16)
end = datetime.date(2021, 11, 19)
df = yfin.download(["AMZN", "F", "BTC-USD"], start, end)["Close"]

# Take a look at the first few rows
print(df.head(10))

Output:

A table showing daily adjusted closing prices for Amazon (AMZN), Bitcoin (BTC-USD), and Ford (F) from November 16 to November 25, 2016, with NaN values indicating non-trading days.

A quick look at the data reveals an important characteristic: we have NaN (Not a Number) values for the stocks on weekends. This is because equities only trade Monday through Friday, while cryptocurrencies trade 24/7. This is a common issue that needs to be handled in data analysis.

We can also get a quick statistical summary of the prices.

# Get summary statistics of the price data
print(df.describe())

Output:

Statistical summary table showing count, mean, standard deviation, minimum, maximum, and quartiles for stock tickers AMZN, BTC-USD, and F.

While this tells us the mean, standard deviation, and range of prices, it’s difficult to compare these assets directly because their price scales are so different. A chart illustrates this perfectly.

# Create a figure with a shared index but different y-axis scales
fig = plt.figure(figsize=(12, 6))
ax1 = fig.add_subplot(111)
ax2 = ax1.twinx()
ax3 = ax1.twinx()

# Plot the 2020 price data for each asset
df["2020-01-01":"2020-12-31"].plot(ax=ax1, y="AMZN", legend=True)
df["2020-01-01":"2020-12-31"].plot(ax=ax2, y="BTC-USD", legend=True, color="g")
df["2020-01-01":"2020-12-31"].plot(ax=ax3, y="F", legend=True, color="r")

# Set the labels for the axes
ax1.set_ylabel("AMZN")
ax2.set_ylabel("BTC-USD")
ax3.set_ylabel("F")
ax3.spines["right"].set_position(("outward", 60))
plt.title("Price Comparison in 2020")
plt.show()

Output:

A line chart comparing the prices of Amazon (AMZN), Ford (F), and Bitcoin (BTC-USD) over the year 2020, with distinct colors representing each asset.

The chart clearly shows why using price alone isn’t ideal. The scale of Bitcoin’s price dwarfs that of Ford, making a visual comparison of their performance nearly impossible.

Calculating Return on Investment

To truly compare these assets, let’s calculate the return on a hypothetical $1,000 investment in each. On our start date of November 21, 2016 (the first weekday in our dataset), the prices were:

  • AMZN: $780.00
  • F: $8.38
  • Bitcoin: $739.25

With $1,000, we could have purchased:

  • AMZN: 1.282 shares
  • F: 119.403 shares
  • Bitcoin: 1.353 units

By our end date of November 18, 2021, the prices had grown significantly. Multiplying our shares by the final prices gives us the future value of our investment:

  • AMZN: 1.282 * $3,696.06 = $4,738.35
  • F: 119.403 * $17.14 = $2,046.57
  • Bitcoin: 1.353 * $56,942.14 = $77,042.72

All three were profitable, but the difference is staggering. Bitcoin was the clear standout. To standardize this, we use the simple return formula:

Simple Return Formula:

R_simple = (P_final − P_initial) / P_initial

This yields the following returns:

  • AMZN: 373.84%
  • F: 104.66%
  • Bitcoin: 7,604.27%

This dramatic difference highlights the concept of the risk-reward tradeoff. Bitcoin, a relatively new and volatile asset, offered an astronomical potential for reward. The more established stocks, while still providing excellent returns, were far less explosive.

Adding Bonds to the Mix

Let’s introduce a fourth asset class: bonds. We’ll use an ETF, the Vanguard Long-Term Bond Index Fund (BLV), as a proxy. Bonds are generally considered safer than stocks. A $1,000 investment in BLV over the same five-year period would have grown to approximately $1,418.11, a return of 41.81%.

This is a much lower return, which is expected. Bondholders have less credit risk than stockholders and receive regular coupon payments. This makes them a lower-risk, and consequently, lower-reward asset class.

Part 2: Measuring Risk and Volatility

Return is only one side of the investment equation. The other is risk, which we can quantify by measuring volatility. Let’s compare two stock indices: the S&P 500 (large-cap stocks) and the Russell 2000 (small-cap stocks).

# Pull 10 years of daily price data
start_indices = datetime.date(2015, 11, 25)
end_indices = datetime.date.today()

prices_indices = yfin.download(["^GSPC", "^RUT"], start_indices, end_indices)["Close"]
prices_indices = prices_indices.rename(columns={"^GSPC": "SP500", "^RUT": "Russell2000"})

print(prices_indices.head())

Output:

Table displaying financial data with dates and corresponding stock prices for two assets.

Price Volatility: High-Low Range

A simple way to get a feel for volatility is to look at the difference between the highest and lowest prices over a period.

Output:

# Calculate the high-low range for the last year of data
currYear = prices_indices.loc[datetime.date.today() - datetime.timedelta(365) : datetime.date.today()]
high_low_range = currYear.max() - currYear.min()
print(high_low_range)

# Standardize by dividing by the current price
standardized_high_low = high_low_range / prices_indices.iloc[-1]
print(standardized_high_low)
Output displaying summary statistics for SP500 and Russell2000 price data, including mean and standard deviation values.

This metric shows the Russell 2000 to be more volatile, which is in line with expectations for smaller companies.

Moving Averages: Smoothing the Noise

A moving average smooths out price data to create a single flowing line, making it easier to identify the underlying trend. The 50-day and 200-day moving averages are common technical indicators.

Output:

# Calculate and plot the 50-day moving average for the S&P 500
prices_indices["SP500_50_day_rolling_avg"] = prices_indices.SP500.rolling(50).mean()

plt.figure(figsize=(12, 5))
plt.plot(prices_indices["SP500"], label="Daily S&P 500 Prices")
plt.plot(prices_indices["SP500_50_day_rolling_avg"], label="50-Day Rolling Avg")
plt.legend()
plt.show()
Line chart showing the daily S&P 500 prices and the 50-day rolling average from 2016 to 2026.

We can use this to create another volatility metric: the average absolute distance between the daily price and the moving average, standardized by the price.

# Moving average volatility
ma_volatility = ((abs(prices_indices - prices_indices.rolling(50).mean())) / prices_indices).mean()
print(ma_volatility)

Output:

Table displaying the calculated standard deviation of daily returns for SP500 and Russell2000 indices, along with the 50-day rolling average.

Again, this confirms that the Russell 2000 has been more volatile.

Standard Deviation: The Analyst’s Choice

While the above metrics are useful, the most popular measure of volatility is standard deviation. It’s crucial to calculate this on returns, not prices, for a fair comparison. Let’s calculate the log returns for our two indices.

# Calculate log returns
log_returns_indices = np.log(prices_indices) - np.log(prices_indices.shift(1))
log_returns_indices = log_returns_indices.dropna()

# Calculate the standard deviation of daily returns
std_dev_returns = log_returns_indices.std()
print(std_dev_returns)

Output:

A table displaying the volatility metrics for the S&P 500 and Russell 2000 indices, including standard deviation and 50-day rolling average.

The standard deviation of daily returns once again confirms that the Russell 2000 is the more volatile of the two indices.

Putting It All Together: A Comparison Function

To make our analysis reproducible, we can wrap these metrics into a single Python function.

import pandas as pd
import datetime
import matplotlib.pyplot as plt
import numpy as np
import yfinance as yfin

def investCompare(startTime, endTime, tickers):
    # Pull price data
    prices = yfin.download(list(tickers.keys()), startTime, endTime)["Close"]
    prices = prices.rename(columns=tickers)

    # Calculate log returns
    returns = np.log(prices) - np.log(prices.shift(1))
    returns = returns.iloc[1:, 0:]

    # Calculate High-Low volatility for the last year
    currYear = prices.loc[datetime.date.today() - datetime.timedelta(365) : datetime.date.today()]
    highLow = (currYear.max() - currYear.min()) / prices.iloc[-1]
    highLow = pd.DataFrame(highLow, columns=["HighMinusLow"])

    # Moving average volatility
    MA = pd.DataFrame(
        ((abs(prices - prices.rolling(50).mean())) / prices).mean(),
        columns=["MovingAverageVolatility"],
    )

    # Combine metrics into a single DataFrame
    investments = pd.merge(highLow, MA, left_index=True, right_index=True)
    investments = pd.merge(
        investments,
        pd.DataFrame(returns.std(), columns=["StandardDeviation"]),
        left_index=True,
        right_index=True,
    )
    investments = pd.merge(
        investments,
        pd.DataFrame(100 * returns.mean(), columns=["Daily Return Percentage"]),
        left_index=True,
        right_index=True,
    )

    return investments.round(3)

# Example usage: Growth vs. Value ETFs
print(investCompare(
    datetime.date(2010, 1, 1), datetime.date.today(), {"VUG": "Growth", "VTV": "Value"}
))

Conclusion: The Journey Continues

This lesson has served as a foundational starting point. We’ve moved from simple price analysis to calculating returns and quantifying risk with various volatility metrics. We’ve seen that different asset classes carry different levels of risk and potential reward. Even within a single asset class like stocks, there are variations between growth, value, and company size.

In the next lesson, we will build upon these concepts, introducing more advanced risk-adjusted return metrics and exploring the statistical distributions that govern financial data.


One response to “Foundations of Financial Analysis: From Returns to Risk”

  1. […] our previous article, we laid the groundwork for financial analysis by exploring returns and the fundamental […]

Leave a Reply

Discover more from SimplifiedZone

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

Continue reading

Discover more from SimplifiedZone

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

Continue reading