I'm looking to generate stock returns with inter-stock correlation in Python. However, the output is not behaving properly and may have accidental temporal correlation causing issues.
This code is designed to generate num_paths of correlated stock returns given a Panda's Series of annualized returns, a DataFrame of constant covariances and a DateIndex of dates (date_index).
from pandas import DataFrame, concat
from scipy.stats import multivariate_normal
def correlated_returns(num_paths, returns, covariances, date_index, periods_per_year=1):
period_returns = (1 + returns) ** (1 / periods_per_year) - 1 if periods_per_year != 1 else returns
mn = multivariate_normal(period_returns, covariances / periods_per_year, allow_singular=True)
digits = len(str(num_paths))
paths = [DataFrame(mn.rvs(size=len(date_index)), index=date_index, columns=returns.index) for _ in range(num_paths)]
keys = [f'Run {str(run_num).zfill(digits)}' for run_num in range(num_paths)]
return concat(paths, axis='columns', keys=keys, names=['Run', 'Returns'])
My test code based on the following covariance matrix shows a few oddities in the results which might be related.
correlation = 0.2 # inter-stock correlation 0.18
annualized_return = 7 / 100 # Simulated return for each stock
stocks = [f'Stock {i}' for i in range(simulated_stocks)]
constituent_weights = DataFrame(1 / simulated_stocks, date_index, stocks)
returns = Series(annualized_return, stocks)
volatilities = Series(volatility, stocks)
correlations = DataFrame(correlation, stocks, stocks)
fill_diagonal(correlations.values, 1)
covariances = correlations.mul(volatilities, axis='index').mul(volatilities, axis='columns')
Over a large number of simulations using equal constituent_weights (above) the index_returns appear to be negatively auto-correlated (using autocorr in Pandas). The annualized_returns of the index come in less than 7% expected when the correlation is greater than 0, but equal to 7% when the correlation is 0. Very wierd.
for simulation in range(simulations):
runs = correlated_returns(1, returns, covariances, date_index, frequency_scale)
return_history = runs['Run 0']
index_returns = return_history.mul(constituent_weights).sum(axis='columns') \
.div(constituent_weights.sum(axis='columns'))
annualized_return = (1 + index_returns[1:]).prod() ** (1 / simulation_years) - 1
Am I misusing multivariate_normal somehow?