\[ % MathJax has no bm package; redefine \bm in terms of \boldsymbol, which it supports natively \newcommand{\bm}[1]{\boldsymbol{#1}} % --- Operators ----------------------------------------------------------- % Expectation operator \DeclareMathOperator{\E}{E} % Variance operator \DeclareMathOperator{\Var}{Var} % Covariance operator \DeclareMathOperator{\Cov}{Cov} % Correlation operator \DeclareMathOperator{\Corr}{Corr} % Rank operator (Spearman rank correlation) \DeclareMathOperator{\Rank}{Rank} % Skewness operator \DeclareMathOperator{\Skewness}{Skewness} % Kurtosis operator \DeclareMathOperator{\Kurtosis}{Kurtosis} % Difference/differential operator (upright d per ISO 80000-2) \newcommand{\Diff}{\mathrm{d}} % --- Risk Measures ------------------------------------------------------- % Value at Risk \DeclareMathOperator{\VaR}{VaR} % Expected Shortfall \DeclareMathOperator{\ES}{ES} % Marginal Value at Risk \DeclareMathOperator{\MVaR}{MVaR} % Component Value at Risk \DeclareMathOperator{\CompVaR}{CVaR} % Incremental Value at Risk \DeclareMathOperator{\IVaR}{IVaR} % Component Expected Shortfall \DeclareMathOperator{\CompES}{CES} % Weighted sensitivity for risk factor in FRTB SBM bucket \DeclareMathOperator{\WS}{WS} % Hedge-benefit ratio for FRTB DRC bucket \DeclareMathOperator{\HBR}{HBR} % Stress scenario risk measure for FRTB NMRF capital add-on \DeclareMathOperator{\SES}{SES} % Jump-to-default exposure for obligor (FRTB DRC) \DeclareMathOperator{\JTD}{JTD} % Loss given default for obligor (FRTB DRC) \DeclareMathOperator{\LGD}{LGD} % Default risk weight for obligor (FRTB DRC) \DeclareMathOperator{\RW}{RW} % ES ratio function \newcommand{\ESratio}{\lambda} % Arbitrary risk measure function (coherence axioms) \newcommand{\RiskMeasure}{\varphi} % Asset position (coherence axioms) \newcommand{\Asset}{A} % First named asset in examples \newcommand{\AssetA}{A} % Second named asset in examples \newcommand{\AssetB}{B} % Third named asset in examples \newcommand{\AssetC}{C} % Constant (risk measure axioms) \newcommand{\Constant}{c} % Bucket-level aggregate sensitivity in FRTB SBM \newcommand{\BucketAgg}{S} % Cross-bucket correlation in FRTB SBM \newcommand{\CrossBucketCorr}{\gamma} % Notional amount for an instrument (FRTB DRC and RRAO) \newcommand{\Notional}{\text{Notional}} % --- Distributions ------------------------------------------------------- % Binomial distribution \DeclareMathOperator{\Binomial}{Binomial} % Uniform distribution \DeclareMathOperator{\Uniform}{Uniform} % Normal distribution \newcommand{\NormalDist}{\mathcal{N}} % Student-t CDF \newcommand{\StudentCDF}{t} % Student-t PDF \newcommand{\StudentPDF}{f} % --- Returns ------------------------------------------------------------- % Price \newcommand{\Price}{P} % Simple (arithmetic) return \newcommand{\SimpleReturns}{R} % Log (compound) return \newcommand{\CompoundReturns}{y} % Matrix of compound returns \newcommand{\ReturnMatrix}{\bm{y}} % Dividend payment \newcommand{\dividend}{d} % --- Volatility ---------------------------------------------------------- % Realised variance \DeclareMathOperator{\RealisedVar}{RV} % Realised volatility (square root of realised variance) \DeclareMathOperator{\RealisedVol}{RVol} % Volatility \newcommand{\Vol}{\sigma} % GARCH intercept parameter \newcommand{\GARCHconst}{\omega} % ARCH coefficient (weight on past squared returns) \newcommand{\ARCHcoeff}{\alpha} % GARCH coefficient (weight on past variance) \newcommand{\GARCHcoeff}{\beta} % EWMA decay factor (typically 0.94) \newcommand{\EWMAdecay}{\lambda} % Standardized residual / error term \newcommand{\StdNormal}{\epsilon} % Leverage parameter in apARCH \newcommand{\APARCHleverage}{\zeta} % Power parameter in apARCH \newcommand{\APARCHpower}{\delta} % Degrees of freedom (Student-t) \newcommand{\DOF}{\nu} % GJR-GARCH leverage parameter \newcommand{\GJRleverage}{\gamma} % GARCH-X external regressor coefficient \newcommand{\RegressorCoeff}{\xi} % Mean (first moment) \newcommand{\Mean}{\mu} % Lag order in volatility models \newcommand{\Lag}{L} % Dummy variable (indicator in regressions) \newcommand{\DummyVar}{D} % --- Portfolio ----------------------------------------------------------- % Portfolio weight (scalar) \newcommand{\weight}{w} % Portfolio weight vector \newcommand{\weights}{\bm{w}} % Covariance matrix \newcommand{\CovMatrix}{\bm{\Sigma}} % Correlation matrix \newcommand{\CorrMatrix}{\bm{C}} % Correlation coefficient \newcommand{\correlation}{\rho} % Number of assets in portfolio \newcommand{\NumberAssets}{K} % Portfolio value \newcommand{\PortfolioValue}{\vartheta} % Asset beta (CAPM) \newcommand{\AssetBeta}{\beta} % Ledoit-Wolf shrinkage intensity \newcommand{\ShrinkageIntensity}{\delta} % Sample covariance matrix \newcommand{\SampleCov}{\bm{S}} % --- Time Series --------------------------------------------------------- % Sample size \newcommand{\SampleSize}{T} % Generic count (number of lags, payments, etc.) \newcommand{\Count}{N} % Estimation window length \newcommand{\EstWindow}{W_E} % Testing window length \newcommand{\TestWindow}{W_T} % Stress window length \newcommand{\StressWindow}{W_S} % AR coefficient \newcommand{\ARcoeff}{\phi} % MA coefficient \newcommand{\MAcoeff}{\psi} % --- Probability --------------------------------------------------------- % Probability level (e.g. 0.01 for 1% VaR) \newcommand{\probability}{p} % Cumulative sorted scenario weight \newcommand{\CumScenarioWeight}{\Omega} % p-quantile of the P and L distribution \newcommand{\Quantile}{q} % Quantile function \newcommand{\QuantileFunction}{Q} % Probability density function \newcommand{\PDF}{f} % Cumulative distribution function \newcommand{\CDF}{F} % Standard normal CDF \newcommand{\NormalCDF}{\Phi} % Inverse standard normal (quantile function) \newcommand{\NormalQuantile}{\Phi^{-1}} % Standard normal density function \newcommand{\NormalPDF}{\phi} % Likelihood function \newcommand{\lik}{\mathcal{L}} % Log-likelihood \newcommand{\LogLikelihood}{\ell} % Parameter vector (MLE estimation) \newcommand{\ParamSet}{\theta} % Parameter space \newcommand{\ParamSpace}{\Theta} % Fisher information matrix \newcommand{\FisherInfo}{\mathcal{I}} % Statistical power (Type II error rate) \newcommand{\StatPower}{\beta} % Significance level (CI coverage complement) \newcommand{\SignifLevel}{\gamma} % --- Options ------------------------------------------------------------- % Call option label \newcommand{\CallOption}{\text{call}} % Put option label \newcommand{\PutOption}{\text{put}} % Strike price \newcommand{\Strike}{X} % Risk-free interest rate \newcommand{\RiskFree}{r_f} % Option maturity (time to expiration) \newcommand{\OptionMaturity}{\tau} % Option delta \newcommand{\OptionDelta}{\Delta} % Option gamma \newcommand{\OptionGamma}{\Gamma} % Option vega \newcommand{\Vega}{\mathcal{V}} % Holding period \newcommand{\HoldingPeriod}{H} % Simulation profit/loss (signed; negative is a loss) \newcommand{\ProfitLoss}{\Pi} % Futures price \newcommand{\Futures}{F} % Hedge ratio \newcommand{\HedgeRatio}{h} % Units of basic asset held \newcommand{\StockHolding}{x^b} % Units of options held \newcommand{\OptionHolding}{x^o} % --- Interest Rates ------------------------------------------------------ % Bond convexity \newcommand{\Convexity}{C} % Interest rate / yield \newcommand{\Yields}{r} % Key rate change, basis points (scalar tenor component); used as \KeyRateChange_k or \KeyRateChange_{t,k}. EXCEPTION to the no-macro-to-macro-indirection rule, decided 2026-08-09: composed from \Yields on purpose, so a future change to the rate glyph propagates here without a follow-up edit. \Yields is defined earlier in generated output (same interest_rates category, TOML order), so expansion order is safe; verify this still holds if either entry is reordered. \newcommand{\KeyRateChange}{\Delta \Yields^{\mathrm{bp}}} % Key rate changes, basis points (vector); used bare or as \KeyRateChanges_t. EXCEPTION to the no-macro-to-macro-indirection rule, decided 2026-08-09: composed from \Yields on purpose, so a future change to the rate glyph propagates here without a follow-up edit. \bm{\Yields} depends on \bm/\boldsymbol correctly expanding a macro argument rather than a bare letter --- confirm this renders correctly the first time either consumer compiles it. \newcommand{\KeyRateChanges}{\Delta \bm{\Yields}^{\mathrm{bp}}} % Dollar value of a basis point \newcommand{\DV}{\text{DV01}} % Mean reversion speed (interest rate models) \newcommand{\MeanReversion}{\varkappa} % Modified duration \newcommand{\ModDur}{D} % Macaulay duration \newcommand{\MacDur}{D_{\text{Mac}}} % Effective duration \newcommand{\EffDur}{D_{\text{eff}}} % DV01 vector \newcommand{\DVvec}{\bm{d}} % Long-run mean rate (Vasicek/CIR/Hull-White) \newcommand{\LongRunRate}{\theta} % Hull-White time-varying drift \newcommand{\HullWhiteDrift}{\varphi} % Cash flow (coupon/principal payment) \newcommand{\CashFlow}{c} % --- Extreme Value ------------------------------------------------------- % Tail index (EVT, Pareto-type tails) \newcommand{\TailIndex}{\iota} % Shape parameter (xi = 1/iota) \newcommand{\ShapeParam}{\varsigma} % Threshold value (EVT) \newcommand{\Threshold}{u} % Extremal index (Leadbetter 1983) \newcommand{\ExtremalIndex}{\theta} % Multivariate extremal coefficient, theta in [1,d]. theta=1 complete tail dependence; theta=d asymptotic independence. Glyph collides with ExtremalIndex and ParamSet; disambiguated at source by macro name. \newcommand{\ExtremalCoef}{\theta} % GEV distribution function \newcommand{\GEV}{\mathcal{H}} % GPD distribution function \newcommand{\GPD}{\mathcal{G}} % GPD scale parameter \newcommand{\GPDscale}{\beta} % GPD scale parameter at a threshold u, distinct from the generic GPD scale \newcommand{\GPDscaleAtThreshold}{\beta_u} % Number of threshold exceedances (EVT) \newcommand{\TailCount}{C} % Sample maximum (block maxima, EVT) \newcommand{\SampleMax}{M} % Kurtosis value (realized, e.g. sample kurtosis) \newcommand{\KurtosisVal}{\kappa} % Pareto scaling constant (EVT) \newcommand{\ParetoConst}{\mathcal{A}} % Generic constant (EVT asymptotics) \newcommand{\RemainderConst}{\mathcal{C}} % Little-o asymptotic notation \newcommand{\LittleO}{o} % Moment order (EVT) \newcommand{\MomentOrder}{\mathscr{m}} % Doubled sample size (EVT block maxima) \newcommand{\DoubleSample}{D} % GEV normalizing location constant \newcommand{\GEVloc}{a} % GEV normalizing scale constant \newcommand{\GEVscale}{b} % Negative return (loss, EVT context) \newcommand{\NegativeReturn}{L} % --- Copulas ------------------------------------------------------------- % Copula function \newcommand{\Copula}{\mathcal{C}} % Copula density \newcommand{\CopulaDensity}{c} % Generator function (Archimedean copulas) \newcommand{\generator}{\varphi} % Copula dependence parameter \newcommand{\CopulaParam}{\theta} % Copula marginal parameter set \newcommand{\MarginalParamSet}{\eta} % Lower tail dependence coefficient \newcommand{\LowerTailDep}{\lambda_L} % Upper tail dependence coefficient \newcommand{\UpperTailDep}{\lambda_U} % Marginal CDF (second variable) \newcommand{\MarginalCDF}{G} % Joint density function \newcommand{\JointDensity}{h} % Marginal density function \newcommand{\MarginalDensity}{g} % Joint distribution function \newcommand{\JointCDF}{H} % Gaussian copula correlation parameter \newcommand{\CopulaCorr}{\rho} % --- Multivariate -------------------------------------------------------- % Diagonal matrix of conditional volatilities \newcommand{\DiagVolD}{\bm{D}} % Idiosyncratic variance matrix (diagonal, factor models) \newcommand{\IdioVarMatrix}{\bm{\Psi}} % Covariance matrix entry (two indices) \newcommand{\CovElement}{\sigma} % Auxiliary matrix in DCC dynamics \newcommand{\DCCauxQ}{\bm{Q}} % BEKK constant matrix \newcommand{\BEKKconst}{\bm{\Omega}} % BEKK ARCH parameter matrix \newcommand{\BEKKarch}{\bm{A}} % BEKK GARCH parameter matrix \newcommand{\BEKKgarch}{\bm{B}} % DCC rescaling diagonal matrix \newcommand{\DCCrescaleZ}{\bm{Z}} % DCC auxiliary matrix element \newcommand{\DCCelement}{q} % Factor loadings matrix (loadings/eigenvectors of the covariance matrix) \newcommand{\FactorLoadings}{\bm{\Lambda}} % Factor subscript label \newcommand{\factor}{\text{factor}} % PCA eigenvalue \newcommand{\Eigenvalue}{\lambda} % Number of factors in factor model \newcommand{\NumberFactors}{m} % Factor return vector \newcommand{\FactorReturn}{\bm{g}} % PCA factor score (scalar; not an eigenvector) \newcommand{\PCAFactor}{F} % BEKK cross-covariance coefficient \newcommand{\BEKKcross}{\delta} % Factor loading element \newcommand{\FactorLoading}{\Lambda} % DCC correlation persistence parameter \newcommand{\DCCxi}{\xi} % DCC news coefficient (weight on recent shocks) \newcommand{\DCCzeta}{\zeta} % --- Simulation ---------------------------------------------------------- % Number of simulation paths \newcommand{\NumberSims}{B} % Cholesky factor of covariance matrix \newcommand{\Cholesky}{\bm{L}} % Uniform random number \newcommand{\UniformDraw}{\tilde{u}} % --- Backtesting --------------------------------------------------------- % Violation ratio (observed/expected violations) \DeclareMathOperator{\ViolRatio}{VR} % Stressed Value at Risk \DeclareMathOperator{\StressedVaR}{SVaR} % QLIKE loss function for variance forecasts \DeclareMathOperator{\QLIKE}{QLIKE} % Mean squared error (forecast loss function) \DeclareMathOperator{\MSE}{MSE} % Mean absolute error (forecast loss function) \DeclareMathOperator{\MAE}{MAE} % Markov transition probability \newcommand{\TransProb}{\pi} % Exception indicator: 1 if y_t < -VaR_t \newcommand{\ExceptionInd}{\eta} % Exception count \newcommand{\ExceptionCount}{\upsilon} % Probability Integral Transform \newcommand{\ProbIntegralTransform}{\hat{u}} % Quantile score (pinball loss function) \newcommand{\QuantileScore}{\mathcal{S}} % Kolmogorov-Smirnov test statistic \newcommand{\KSstat}{D} % Test statistic (generic base letter) \newcommand{\TestStat}{J} % --- General ------------------------------------------------------------- % Regression intercept \newcommand{\RegressionIntercept}{a} % Regression slope \newcommand{\RegressionSlope}{\beta} % Risk factor \newcommand{\RiskFactor}{x} % Forecast horizon \newcommand{\ForecastHorizon}{h} % Block length (bootstrap) \newcommand{\BlockLength}{\ell} % Capital tau variant \newcommand{\DeliveryTime}{\Upsilon} % Today's calendar time in years (option pricing input; distinct from trading-date index t) \newcommand{\CalendarTime}{t^*} % Probability measure \newcommand{\ProbMeasure}{\mathbb{P}} % Risk-neutral measure \newcommand{\RiskNeutral}{\mathbb{Q}} % Pricing function (Black-Scholes, bond pricing, etc.) \newcommand{\PricingFn}{V} % Indicator function \newcommand{\Indicator}{\bm{1}} % Vector of ones \newcommand{\OnesVector}{\bm{1}} % Risk factor sensitivity (first-order P&L) \newcommand{\Sensitivity}{\delta} % Second-order sensitivity (convexity/gamma) \newcommand{\ConvexitySens}{\gamma} % Output floor percentage (Basel) \newcommand{\OutputFloor}{\alpha} % --- Subscript Labels ---------------------------------------------------- % Subscript label: annualized \newcommand{\Annual}{a} % Subscript label: implied \newcommand{\Implied}{I} % Subscript label: portfolio \newcommand{\Portfolio}{\pi} % --- Text Abbreviations -------------------------------------------- % S&P 500 index \newcommand{\SP}{\text{S\&P-500}} % Student-t distribution (text) \newcommand{\St}{\text{Student-t}} % Geopolitical Risk index (Caldara and Iacoviello) \newcommand{\GPR}{\text{GPR}} \]

11  Data used here

These three files drive the examples in these notes. EODHD supplies the daily US data. The S&P 500 index series runs from 1990 to 2025, while the total return series and the stock prices run from 2003 to 2022. The files are at https://www.financialriskforecasting.com/data.

  1. SP500, daily prices of the Standard & Poor’s 500 index (S&P 500), used later for single-series examples;
  2. SP500TR, total returns on the S&P 500;
  3. Stocks, the prices of different stocks, chosen to represent the various sectors of the US economy, and including both winners and losers. The file contains both unadjusted and adjusted prices.

The first three lines of sp500.csv are:

date,Close
19900102,359.69
19900103,358.76

The last two:

20250627,6173.07
20250630,6204.95

The start of stocks.csv looks like:

ticker,date,Close,Adjusted_close
MCD,20030102,16.55,9.6942
MCD,20030103,16.12,9.4423

The end:

INTC,20221229,26.21,25.8945
INTC,20221230,26.43,26.1118

We show the steps here because the logic matters. Later chapters call ProcessRawData(), which does the same work in one line but also attaches time-series columns and returns the index series alongside the stock matrices.

Log returns are calculated as:

\[ \CompoundReturns_t = \log \Price_t - \log \Price_{t-1}. \]

11.1 Libraries

Each language needs a little setup before the first file is read.

library(reshape2)
import pandas as pd
import numpy as np
using CSV
using DataFrames

11.2 The index series

sp500.csv holds one price per trading day. Reading it, converting the date column and differencing the logged price gives the return series that most of the book uses.

We import the CSV file into R as a data.frame using read.csv().

sp500 = read.csv('data/sp500.csv')
class(sp500)
str(sp500)
[1] "data.frame"
'data.frame':   8939 obs. of  2 variables:
 $ date : int  19900102 19900103 19900104 19900105 19900108 19900109 19900110 19900111 19900112 19900115 ...
 $ Close: num  360 359 356 352 354 ...
head(sp500, 2)
tail(sp500, 2)
      date  Close
1 19900102 359.69
2 19900103 358.76
         date   Close
8938 20250627 6173.07
8939 20250630 6204.95

The name Close is not convenient, so we rename it.

names(sp500)[2] = "price"

We add a new column with log returns. We will not have an observation for day 1, so we add NA for the first value and then remove it.

sp500$date = as.Date(as.character(sp500$date), format = "%Y%m%d")
sp500$y = c(NA, diff(log(sp500$price)))
sp500 = sp500[!is.na(sp500$y), ]
head(sp500, 2)
        date  price            y
2 1990-01-03 358.76 -0.002588908
3 1990-01-04 355.67 -0.008650307

We import the CSV file using pd.read_csv().

sp500 = pd.read_csv('data/sp500.csv')
print(type(sp500))
print(sp500.dtypes)
<class 'pandas.core.frame.DataFrame'>
date       int64
Close    float64
dtype: object
print(sp500.head(2))
print(sp500.tail(2))
       date   Close
0  19900102  359.69
1  19900103  358.76
          date    Close
8937  20250627  6173.07
8938  20250630  6204.95

We rename the Close column to price.

sp500 = sp500.rename(columns={'Close': 'price'})

We add log returns and remove the first row which has NaN.

sp500['date'] = pd.to_datetime(sp500['date'], format='%Y%m%d')
sp500['y'] = np.log(sp500['price']).diff()
sp500 = sp500.dropna(subset=['y'])
print(sp500.head(2))
        date   price         y
1 1990-01-03  358.76 -0.002589
2 1990-01-04  355.67 -0.008650

We import the CSV file using CSV.read().

using CSV, DataFrames
sp500 = CSV.read("data/sp500.csv", DataFrame);
println(typeof(sp500))
println(names(sp500))
DataFrame
["date", "Close"]
using CSV, DataFrames
sp500 = CSV.read("data/sp500.csv", DataFrame);
println(first(sp500, 2))
println(last(sp500, 2))
2×2 DataFrame
 Row │ date      Close
     │ Int64     Float64
─────┼───────────────────
   1 │ 19900102   359.69
   2 │ 19900103   358.76
2×2 DataFrame
 Row │ date      Close
     │ Int64     Float64
─────┼───────────────────
   1 │ 20250627  6173.07
   2 │ 20250630  6204.95

We rename the Close column to price.

using CSV, DataFrames
sp500 = CSV.read("data/sp500.csv", DataFrame);
rename!(sp500, :Close => :price);

We add log returns and remove the first row which has missing.

using CSV, DataFrames, Dates
sp500 = CSV.read("data/sp500.csv", DataFrame);
rename!(sp500, :Close => :price);
sp500.date = Date.(string.(sp500.date), DateFormat("yyyymmdd"));
sp500.y = [missing; diff(log.(sp500.price))];
sp500 = sp500[.!ismissing.(sp500.y), :];
println(first(sp500, 2))
2×3 DataFrame
 Row │ date        price    y
     │ Date        Float64  Float64?
─────┼──────────────────────────────────
   1 │ 1990-01-03   358.76  -0.00258891
   2 │ 1990-01-04   355.67  -0.00865031

11.3 The total return series

sp500tr.csv has the same shape, so the same three operations apply. The difference is in what the series means, not in how it is handled.

The S&P 500 TR (Total Return) index includes reinvested dividends, unlike the price index above. We process it the same way.

sp500tr = read.csv('data/sp500tr.csv')
names(sp500tr)[2] = "price"
sp500tr$date = as.Date(as.character(sp500tr$date), format = "%Y%m%d")
sp500tr$y = c(NA, diff(log(sp500tr$price)))
sp500tr = sp500tr[!is.na(sp500tr$y), ]

The S&P 500 TR (Total Return) index includes reinvested dividends, unlike the price index above. We process it the same way.

sp500tr = pd.read_csv('data/sp500tr.csv')
sp500tr = sp500tr.rename(columns={'Close': 'price'})
sp500tr['date'] = pd.to_datetime(sp500tr['date'], format='%Y%m%d')
sp500tr['y'] = np.log(sp500tr['price']).diff()
sp500tr = sp500tr.dropna(subset=['y'])

The S&P 500 TR (Total Return) index includes reinvested dividends, unlike the price index above. We process it the same way.

using CSV, DataFrames, Dates
sp500tr = CSV.read("data/sp500tr.csv", DataFrame);
rename!(sp500tr, :Close => :price);
sp500tr.date = Date.(string.(sp500tr.date), DateFormat("yyyymmdd"));
sp500tr.y = [missing; diff(log.(sp500tr.price))];
sp500tr = sp500tr[.!ismissing.(sp500tr.y), :];

11.4 The stock panel

stocks.csv is stacked long, one row per ticker per day, with unadjusted and adjusted prices side by side. We keep the adjusted column.

stocks = read.csv('data/stocks.csv')
str(stocks)
'data.frame':   30210 obs. of  4 variables:
 $ ticker        : chr  "MCD" "MCD" "MCD" "MCD" ...
 $ date          : int  20030102 20030103 20030106 20030107 20030108 20030109 20030110 20030113 20030114 20030115 ...
 $ Close         : num  16.6 16.1 16.6 16.7 16.8 ...
 $ Adjusted_close: num  9.69 9.44 9.75 9.76 9.86 ...

The data frame has four columns. There are columns for ticker symbols, dates and both unadjusted and adjusted stock prices.

dim(stocks)
colnames(stocks)
unique(stocks$ticker)
[1] 30210     4
[1] "ticker"         "date"           "Close"          "Adjusted_close"
[1] "MCD"  "DIS"  "AAPL" "GE"   "JPM"  "INTC"

We rename the columns to distinguish the raw close from the adjusted price and parse date to a Date.

names(stocks)[3:4] = c("UnAdjustedPrice", "price")
stocks$date = as.Date(as.character(stocks$date), format = "%Y%m%d")
stocks = pd.read_csv('data/stocks.csv')
print(stocks.dtypes)
print(stocks.shape)
print(stocks['ticker'].unique())
ticker             object
date                int64
Close             float64
Adjusted_close    float64
dtype: object
(30210, 4)
['MCD' 'DIS' 'AAPL' 'GE' 'JPM' 'INTC']

We rename the columns and parse date to a datetime.

stocks = stocks.rename(columns={'Close': 'UnAdjustedPrice', 'Adjusted_close': 'price'})
stocks['date'] = pd.to_datetime(stocks['date'], format='%Y%m%d')
using CSV, DataFrames
stocks = CSV.read("data/stocks.csv", DataFrame);
println(names(stocks))
println(size(stocks))
println(unique(stocks.ticker))
["ticker", "date", "Close", "Adjusted_close"]
(30210, 4)
String7["MCD", "DIS", "AAPL", "GE", "JPM", "INTC"]

We rename the columns and parse date to a Date.

using CSV, DataFrames, Dates
stocks = CSV.read("data/stocks.csv", DataFrame);
rename!(stocks, :Close => :UnAdjustedPrice, :Adjusted_close => :price);
stocks.date = Date.(string.(stocks.date), DateFormat("yyyymmdd"));

11.5 Reshaping the panel

The stacked layout is convenient to store and awkward to compute with. Every model in the book wants a matrix with one column per stock and one row per date, so the last step pivots the panel and takes returns column by column.

The analysis needs one column per stock, so we reshape stocks into one row per date and one column per stock. We use reshape2 for this.

Price = dcast(stocks, date ~ ticker, value.var = "price")
head(Price, 2)
UnAdjustedPrice = dcast(stocks, date ~ ticker, value.var = "UnAdjustedPrice")
        date   AAPL     DIS      GE   INTC     JPM    MCD
1 2003-01-02 0.2250 13.7547 88.5027 9.7865 14.5077 9.6942
2 2003-01-03 0.2265 13.8344 88.2248 9.6985 14.7929 9.4423

We compute returns for each stock.

Return = Price
for (i in 2:dim(Price)[2]) Return[, i] = c(NA, diff(log(Price[, i])))

Remove the NA values.

mask = complete.cases(Return[, -1])
Price = Price[mask, ]
UnAdjustedPrice = UnAdjustedPrice[mask, ]
Return = Return[mask, ]
head(Return, 2)
        date        AAPL        DIS           GE         INTC        JPM
2 2003-01-03 0.006644543 0.00577766 -0.003144957 -0.009032651 0.01946779
3 2003-01-06 0.000000000 0.04999279  0.025268359  0.037966760 0.07570013
          MCD
2 -0.02632817
3  0.03234458

We store the result in one object. In R that object is a list, and in Python and Julia a dictionary.

x = list(Return = Return, Price = Price, UnAdjustedPrice = UnAdjustedPrice)

We reshape using pivot() so each row is a date and each column is a stock.

Price = stocks.pivot(index='date', columns='ticker', values='price').reset_index()
Price = Price[['date'] + sorted(Price.columns.drop('date'))]
print(Price.head(2))
UnAdjustedPrice = stocks.pivot(index='date', columns='ticker', values='UnAdjustedPrice').reset_index()
UnAdjustedPrice = UnAdjustedPrice[['date'] + sorted(UnAdjustedPrice.columns.drop('date'))]
ticker       date    AAPL      DIS       GE    INTC      JPM     MCD
0      2003-01-02  0.2250  13.7547  88.5027  9.7865  14.5077  9.6942
1      2003-01-03  0.2265  13.8344  88.2248  9.6985  14.7929  9.4423

We compute returns for each stock.

Return = Price.copy()
for col in Return.columns[1:]:
    Return[col] = np.log(Price[col]).diff()

Remove the NaN values.

mask = Return.iloc[:, 1:].notna().all(axis=1)
Price = Price[mask].reset_index(drop=True)
UnAdjustedPrice = UnAdjustedPrice[mask].reset_index(drop=True)
Return = Return[mask].reset_index(drop=True)
print(Return.head(2))
ticker       date      AAPL       DIS        GE      INTC       JPM       MCD
0      2003-01-03  0.006645  0.005778 -0.003145 -0.009033  0.019468 -0.026328
1      2003-01-06  0.000000  0.049993  0.025268  0.037967  0.075700  0.032345

We can store everything in a dictionary.

x = {'Return': Return, 'Price': Price, 'UnAdjustedPrice': UnAdjustedPrice}

We reshape using unstack() so each row is a date and each column is a stock.

using CSV, DataFrames, Dates
stocks = CSV.read("data/stocks.csv", DataFrame);
rename!(stocks, :Close => :UnAdjustedPrice, :Adjusted_close => :price);
stocks.date = Date.(string.(stocks.date), DateFormat("yyyymmdd"));
Price = unstack(stocks, :date, :ticker, :price);
Price = Price[:, ["date"; sort(names(Price)[2:end])]];
println(first(Price, 2))
UnAdjustedPrice = unstack(stocks, :date, :ticker, :UnAdjustedPrice);
UnAdjustedPrice = UnAdjustedPrice[:, ["date"; sort(names(UnAdjustedPrice)[2:end])]];
2×7 DataFrame
 Row │ date        AAPL      DIS       GE        INTC      JPM       MCD
     │ Date        Float64?  Float64?  Float64?  Float64?  Float64?  Float64?
─────┼────────────────────────────────────────────────────────────────────────
   1 │ 2003-01-02    0.225    13.7547   88.5027    9.7865   14.5077    9.6942
   2 │ 2003-01-03    0.2265   13.8344   88.2248    9.6985   14.7929    9.4423

We compute returns for each stock.

using CSV, DataFrames, Dates
stocks = CSV.read("data/stocks.csv", DataFrame);
rename!(stocks, :Close => :UnAdjustedPrice, :Adjusted_close => :price);
stocks.date = Date.(string.(stocks.date), DateFormat("yyyymmdd"));
Price = unstack(stocks, :date, :ticker, :price);
Price = Price[:, ["date"; sort(names(Price)[2:end])]];
UnAdjustedPrice = unstack(stocks, :date, :ticker, :UnAdjustedPrice);
UnAdjustedPrice = UnAdjustedPrice[:, ["date"; sort(names(UnAdjustedPrice)[2:end])]];

Return = copy(Price);
for col in names(Return)[2:end]
    Return[!, col] = [missing; diff(log.(Price[!, col]))]
end;

Remove the missing values.

using CSV, DataFrames, Dates
stocks = CSV.read("data/stocks.csv", DataFrame);
rename!(stocks, :Close => :UnAdjustedPrice, :Adjusted_close => :price);
stocks.date = Date.(string.(stocks.date), DateFormat("yyyymmdd"));
Price = unstack(stocks, :date, :ticker, :price);
Price = Price[:, ["date"; sort(names(Price)[2:end])]];
UnAdjustedPrice = unstack(stocks, :date, :ticker, :UnAdjustedPrice);
UnAdjustedPrice = UnAdjustedPrice[:, ["date"; sort(names(UnAdjustedPrice)[2:end])]];

Return = copy(Price);
for col in names(Return)[2:end]
    Return[!, col] = [missing; diff(log.(Price[!, col]))]
end

mask = completecases(Return[:, 2:end]);
Price = Price[mask, :];
UnAdjustedPrice = UnAdjustedPrice[mask, :];
Return = Return[mask, :];
println(first(Return, 2))
2×7 DataFrame
 Row │ date        AAPL        DIS         GE           INTC         JPM        MCD
     │ Date        Float64?    Float64?    Float64?     Float64?     Float64?   Float64?
─────┼─────────────────────────────────────────────────────────────────────────────────────
   1 │ 2003-01-03  0.00664454  0.00577766  -0.00314496  -0.00903265  0.0194678  -0.0263282
   2 │ 2003-01-06  0.0         0.0499928    0.0252684    0.0379668   0.0757001   0.0323446

We can store everything in a dictionary.

using CSV, DataFrames, Dates
stocks = CSV.read("data/stocks.csv", DataFrame);
rename!(stocks, :Close => :UnAdjustedPrice, :Adjusted_close => :price);
stocks.date = Date.(string.(stocks.date), DateFormat("yyyymmdd"));
Price = unstack(stocks, :date, :ticker, :price);
Price = Price[:, ["date"; sort(names(Price)[2:end])]];
UnAdjustedPrice = unstack(stocks, :date, :ticker, :UnAdjustedPrice);
UnAdjustedPrice = UnAdjustedPrice[:, ["date"; sort(names(UnAdjustedPrice)[2:end])]];

Return = copy(Price);
for col in names(Return)[2:end]
    Return[!, col] = [missing; diff(log.(Price[!, col]))]
end

mask = completecases(Return[:, 2:end]);
Price = Price[mask, :];
UnAdjustedPrice = UnAdjustedPrice[mask, :];
Return = Return[mask, :];

x = Dict("Return" => Return, "Price" => Price, "UnAdjustedPrice" => UnAdjustedPrice);

11.6 Summary

ProcessRawData() performs every step above in one call and is what later chapters use. It returns the index series, the total return series and the stock matrices together, with the date columns already typed, so a chapter that needs data starts with one line rather than thirty. The volatility and risk chapters all begin that way.