Basic Financial Analysis

This tutorial demonstrates how to perform fundamental financial analysis using openseries with real market data.

Setting Up

First, let’s import the necessary libraries and download some data:

import yfinance as yf
import pandas as pd
import numpy as np
from openseries import OpenTimeSeries, OpenFrame
from datetime import date, datetime

# Download S&P 500 data for the last 5 years
ticker = yf.Ticker("^GSPC")
data = ticker.history(period="5y")

# Create OpenTimeSeries
sp500 = OpenTimeSeries.from_df(
     dframe=data['Close']
)

# Set a descriptive label
sp500.set_new_label(lvl_zero="S&P 500 Index")

print(f"Loaded {sp500.length} observations")
print(f"Date range: {sp500.first_idx} to {sp500.last_idx}")

Basic Performance Metrics

Let’s calculate the fundamental performance metrics:

# Total return over the period
total_return = sp500.value_ret
print(f"Total Return: {total_return:.2%}")

# Annualized return (CAGR)
annual_return = sp500.geo_ret
print(f"Annualized Return (CAGR): {annual_return:.2%}")

# Arithmetic mean return
arithmetic_return = sp500.arithmetic_ret
print(f"Arithmetic Mean Return: {arithmetic_return:.2%}")

# Time period analysis
print(f"Investment period: {sp500.yearfrac:.2f} years")
print(f"Number of observations: {sp500.length}")
print(f"Periods per year: {sp500.periods_in_a_year:.1f}")

Risk Analysis

Now let’s examine the risk characteristics:

# Volatility (annualized standard deviation)
volatility = sp500.vol
print(f"Annualized Volatility: {volatility:.2%}")

# Downside deviation (volatility of negative returns only)
downside_vol = sp500.downside_deviation
print(f"Downside Deviation: {downside_vol:.2%}")

# Value at Risk (95% confidence level)
var_95 = sp500.var_down
print(f"95% Value at Risk (daily): {var_95:.2%}")

# Conditional Value at Risk (Expected Shortfall)
cvar_95 = sp500.cvar_down
print(f"95% CVaR (daily): {cvar_95:.2%}")

# Maximum single-day loss
worst_day = sp500.worst
print(f"Worst single day: {worst_day:.2%}")

Risk-Adjusted Returns

Calculate risk-adjusted performance metrics:

# Sharpe Ratio (return per unit of total risk)
sharpe_ratio = sp500.ret_vol_ratio
print(f"Sharpe Ratio: {sharpe_ratio:.2f}")

# Sortino Ratio (return per unit of downside risk)
sortino_ratio = sp500.sortino_ratio
print(f"Sortino Ratio: {sortino_ratio:.2f}")

# Kappa-3 Ratio (penalizes larger downside deviations more)
kappa3_ratio = sp500.kappa3_ratio
print(f"Kappa-3 Ratio: {kappa3_ratio:.2f}")

# Omega Ratio
omega_ratio = sp500.omega_ratio
print(f"Omega Ratio: {omega_ratio:.2f}")

Drawdown Analysis

Analyze drawdowns to understand downside risk:

# Maximum drawdown
max_drawdown = sp500.max_drawdown
max_dd_date = sp500.max_drawdown_date
print(f"Maximum Drawdown: {max_drawdown:.2%}")
print(f"Max Drawdown Date: {max_dd_date}")

# Create drawdown series for visualization (modifies original)
sp500.to_drawdown_series()

# Plot drawdowns
sp500.plot_series()
# This will open an interactive plot in your browser

# Worst calendar year drawdown
worst_year_dd = sp500.max_drawdown_cal_year
print(f"Worst Calendar Year Drawdown: {worst_year_dd:.2%}")

Distribution Analysis

Examine the return distribution characteristics:

# Convert to returns for distribution analysis (modifies original)
sp500.value_to_ret()

# Note: value_to_ret() modifies the original series in place
# Restore the original series for further analysis
sp500 = OpenTimeSeries.from_df(dframe=data['Close'])
sp500.set_new_label(lvl_zero="S&P 500 Index")

# Skewness (asymmetry of the distribution)
skewness = sp500.skew
print(f"Skewness: {skewness:.2f}")
if skewness < 0:
     print("  → Negative skew: more extreme negative returns")
elif skewness > 0:
     print("  → Positive skew: more extreme positive returns")

# Kurtosis (tail heaviness)
kurtosis = sp500.kurtosis
print(f"Kurtosis: {kurtosis:.2f}")
if kurtosis > 3:
     print("  → Fat tails: more extreme returns than normal distribution")

# Percentage of positive days
positive_share = sp500.positive_share
print(f"Positive Days: {positive_share:.1%}")

# Current Z-score (how unusual is the last return?)
z_score = sp500.z_score
print(f"Last Return Z-score: {z_score:.2f}")

Monthly and Annual Analysis

Break down performance by different time periods:

# Resample to monthly data (modifies original)
sp500.resample_to_business_period_ends(freq="BME")
print(f"Monthly observations: {sp500.length}")

# Monthly metrics
monthly_return = sp500.geo_ret
monthly_vol = sp500.vol
print(f"Monthly Return (annualized): {monthly_return:.2%}")
print(f"Monthly Volatility (annualized): {monthly_vol:.2%}")

# Worst month
worst_month = sp500.worst_month
print(f"Worst Month: {worst_month:.2%}")

# Annual data (modifies original)
sp500.resample_to_business_period_ends(freq="BYE")
print(f"Annual observations: {sp500.length}")

Calendar Year Returns

# Calculate calendar year returns
years = range(2019, 2025)  # Adjust based on your data range

for year in years:
     # This may fail if no data exists for the year
     year_return = sp500.value_ret_calendar_period(year=year)
     print(f"{year}: {year_return:.2%}")

Rolling Analysis

Analyze how metrics change over time:

# 252-day (1-year) rolling volatility
rolling_vol = sp500.rolling_vol(observations=252)
print(f"Rolling volatility calculated for {len(rolling_vol)} periods")

# 30-day rolling returns
rolling_returns = sp500.rolling_return(observations=30)

# Plot rolling volatility
# Convert to OpenTimeSeries for plotting
vol_dates = rolling_vol.index.strftime('%Y-%m-%d').tolist()
vol_values = rolling_vol.iloc[:, 0].tolist()

vol_series = OpenTimeSeries.from_arrays(
     dates=vol_dates,
     values=vol_values,
     name="Rolling Volatility"
)

vol_series.plot_series()

Comprehensive Report

Get all metrics at once:

# Generate comprehensive metrics report
all_metrics = sp500.all_properties()
print("\n=== COMPREHENSIVE ANALYSIS REPORT ===")
print(all_metrics)

# Save to Excel for further analysis
sp500.to_xlsx(filename="sp500_analysis.xlsx")
all_metrics.to_excel(excel_writer="sp500_metrics.xlsx", engine="openpyxl")

Visualization

Create various visualizations:

# Price chart
sp500.plot_series()

# Returns bar plot and histogram
returns = sp500.from_deepcopy()
returns.value_to_ret()
returns.plot_bars()
returns.plot_histogram()

# Drawdown chart
sp500.to_drawdown_series()
sp500.plot_series()

Comparison with Benchmark

Let’s compare with a bond index:

# Download bond data (10-year Treasury)
bond_ticker = yf.Ticker("^TNX")
bond_data = bond_ticker.history(period="5y")

# Create bond series (using yield data)
bonds = OpenTimeSeries.from_df(
     dframe=bond_data['Close']
)
bonds.set_new_label(lvl_zero="10Y Treasury Yield")

# Create frame for comparison
comparison_frame = OpenFrame(constituents=[sp500, bonds])

# Compare metrics
comparison_metrics = comparison_frame.all_properties()
print("\n=== ASSET COMPARISON ===")
print(comparison_metrics)

# Calculate correlation
correlation_matrix = comparison_frame.correl_matrix
print("\n=== CORRELATION MATRIX ===")
print(correlation_matrix)

Advanced Risk Metrics

Calculate some advanced risk measures:

# VaR at different confidence levels
var_90 = sp500.var_down_func(level=0.90)
var_95 = sp500.var_down_func(level=0.95)
var_99 = sp500.var_down_func(level=0.99)

print(f"90% VaR: {var_90:.2%}")
print(f"95% VaR: {var_95:.2%}")
print(f"99% VaR: {var_99:.2%}")

# CVaR at different confidence levels
cvar_90 = sp500.cvar_down_func(level=0.90)
cvar_95 = sp500.cvar_down_func(level=0.95)
cvar_99 = sp500.cvar_down_func(level=0.99)

print(f"90% CVaR: {cvar_90:.2%}")
print(f"95% CVaR: {cvar_95:.2%}")
print(f"99% CVaR: {cvar_99:.2%}")

# Implied volatility from VaR (assuming normal distribution)
vol_from_var = sp500.vol_from_var
print(f"Volatility implied from VaR: {vol_from_var:.2%}")
print(f"Actual volatility: {sp500.vol:.2%}")

Summary and Interpretation

print("\n=== INVESTMENT SUMMARY ===")
print(f"Asset: {sp500.label}")
print(f"Period: {sp500.first_idx} to {sp500.last_idx}")
print(f"Total Return: {sp500.value_ret:.2%}")
print(f"Annualized Return: {sp500.geo_ret:.2%}")
print(f"Annualized Volatility: {sp500.vol:.2%}")
print(f"Sharpe Ratio: {sp500.ret_vol_ratio:.2f}")
print(f"Maximum Drawdown: {sp500.max_drawdown:.2%}")
print(f"95% VaR (daily): {sp500.var_down:.2%}")

# Risk assessment
if sp500.ret_vol_ratio > 1.0:
     print("✓ Good risk-adjusted returns (Sharpe > 1.0)")
else:
     print("⚠ Moderate risk-adjusted returns (Sharpe < 1.0)")

if abs(sp500.max_drawdown) < 0.20:
     print("✓ Moderate maximum drawdown (< 20%)")
else:
     print("⚠ Significant maximum drawdown (> 20%)")

This tutorial provides a comprehensive foundation for financial analysis using openseries. You can adapt these techniques for any financial time series data.