How to perform scenario analysis of a financial portfolio in Python | by Gianluca Malato | Towards Data Science

Financial investors know that nobody can predict the future, but they also know that they must make at least a probabilistic idea of what the future can be. That’s why scenario analysis is a very powerful tool in an investor’s toolbox.
Scenario analysis is a discipline that tries to give a probabilistic view of the possible future scenarios that may happen in relationship to a phenomenon.
In a previous article, I’ve shown you how to perform portfolio optimization in R using a genetic algorithm. While portfolio optimization is a science, scenario analysis is almost like an art. It often starts from some assumptions and then simulates many future scenarios using Monte Carlo techniques. The higher the number of simulated scenarios, the higher the precision. Finally, the results of all the simulations are analyzed. For example, typical scenario analysis of a financial portfolio can be related to risk (e.g. what’s the probability that our portfolio will lose 3% within 30 days).
In this article, I’ll show you how to apply scenario analysis to a financial portfolio. For this example, we’ll simulate a portfolio made by Microsoft (50%), Apple (20%) and Google (30%) stocks using their daily historical data from January 1 2010 to December 31 2019. We’ll correct their average return in a more pessimistic way than it was in the past and calculate the cumulative expected return of our portfolio, a confidence interval and the expected Sharpe ratio.
Everything will be done in Python. You can get the full notebook on GitHub here: https://github.com/gianlucamalato/machinelearning/blob/master/Portfolio_scenario_analysis.ipynb
First of all, we need to install the yfinance library. It allows us to download the stock prices directly from our notebook.
!pip install yfinance
Then we can import some useful libraries:
import yfinanceimport pandas as pdimport numpy as npimport matplotlib.pyplot as plt
For each stock, we want to calculate the daily return of the close price P from day t to day t+1, which is defined as
Fortunately, Python series provide the useful pct_change method that calculates this formula for us.
Let’s define the portfolio composition and the loop that fills a pandas data frame with the daily returns of each stock.
portfolio_composition = [('MSFT',0.5),('AAPL',0.2),('GOOG',0.3)]returns = pd.DataFrame({})for t in portfolio_composition:  name = t[0]  ticker = yfinance.Ticker(name)  data = ticker.history(interval="1d",     start="2010-01-01",end="2019-12-31")    data['return_%s' % (name)] = data['Close'].pct_change(1)  returns = returns.join(data[['return_%s' % (name)]],     how="outer").dropna()
The returns data frame looks as follows:
Now we are ready to simulate our data.
If we want to simulate m days in the future we just have to:
Take the original returns time series of a stock
Select m values with replacement, taken randomly and uniformly
That’s it. It’s very similar to bootstrap, which is a resampling technique. This method assumes that the future returns are a random resampling from the past returns. It’s a strong approximation, but it can be a good start point.
This simulation can be easily performed with a single line of code as in the following function:
def simulate_returns(historical_returns,forecast_days):  return historical_returns.sample(n = forecast_days,                 replace = True).reset_index(drop = True)
If we try to simulate 1000 days, we get:
simulate_returns(returns['return_AAPL'],1000)
Let’s now create the necessary function for simulating a portfolio.
A portfolio is a combination of returns and weights. Given the returns Ri of the i-th stock and given the weight wi of each stock in the portfolio (all the weights must sum up to 1), the portfolio return R can be expressed as:
that is the weighted sum of the stock returns.
This is the function that performs all the calculations starting from the returns data frame we have created before:
def simulate_portfolio(historical_returns,    composition,    forecast_days):  result = 0  for t in composition:    name,weight = t[0],t[1]    s = simulate_returns(historical_returns['return_%s' % (name)],       forecast_days)    result = result + s * weight    return(result)
Let’s see what happens if we simulate 10 days of our portfolio:
simulate_portfolio(returns,portfolio_composition,10)
This may be enough for portfolio simulation, but we want something more, that is the what-if analysis.
If we perform portfolio simulation as shown before, we are simply saying that the future returns are a random sample of the past returns. We already know this isn’t completely true. Moreover, maybe we are performing scenario analysis because we want to know what happens if certain conditions will occur. For example, what happens if the average daily return of each stock is lower than its historical value?
The historical average returns are:
Let’s try to simulate what happens if the average returns drop by -0.0001 for MSFT, -0.001 for AAPL and -0.0005 for GOOG. We must subtract these quantities from each stock and then simulate the future portfolios with the new, modified data.
We’ll add these corrections directly to the portfolio_composition list (they are the third component of each tuple):
portfolio_composition = [   ('MSFT', 0.5,-0.0001),    ('AAPL', 0.2,-0.001),    ('GOOG', 0.3,-0.0005)]
We can then redefine all the previously defined function.
This is the function that simulates the modified stocks:
def simulate_modified_returns(      historical_returns,      forecast_days,      correct_mean_by):  h = historical_returns.copy()  new_series = h + correct_mean_by  return new_series.sample(n=forecast_days,       replace = True).reset_index(drop=True)
This is the function that simulates the modified portfolio:
def simulate_modified_portfolio(   historical_returns,   composition,   forecast_days):    result = 0    for t in composition:    name,weight,correction = t[0],t[1],t[2]    s = simulate_modified_returns(       historical_returns['return_%s' % (name)],        forecast_days,correction    )        result = result + s * weight    return(result)
Finally, we can define a function that performs portfolio simulation many times, generating many possible scenarios:
def simulation(  historical_returns,  composition,  forecast_days,  n_iterations):     simulated_portfolios = None   for i in range(n_iterations):       sim = simulate_modified_portfolio(         historical_returns,         composition,         forecast_days       )   sim_port = pd.DataFrame({'returns_%d' % (i) : sim})   if simulated_portfolios is None:         simulated_portfolios = sim_port   else:         simulated_portfolios = simulated_portfolios.join(sim_port)       return simulated_portfolios
Now we are ready to start our simulations and analyze the results.
Let’s simulate 20 days in the future calculating our statistics on 200 different scenarios.
forecast_days = 20n_iterations = 200
We can now run the simulations:
simulated_portfolios = simulation(returns,    portfolio_composition,forecast_days,n_iterations)
This function may take some seconds or even minutes if the number of iterations is high.
This is the result:
Each column is a scenario of a simulated portfolio and each row is a single day in the future.
Taken the daily returns of a portfolio, we can build the return after N days with the compound interest formula:
Since the returns are quite small numbers, we can approximate this expression as follows:
That is, the return after N days is the sum of the returns of the N days.
We can now calculate some observables from this dataset.
Scenario analysis can be used as a risk management tool. For this purpose, we may want to calculate the 5th and the 95th percentile of the future cumulative return. The former tells you that there’s a 5% chance that our portfolio will underperform this value, the latter tells you there’s a 5% chance your portfolio will outperform this value. We also want to check how the average portfolio will perform.
Here follows the code that calculates these 3 curves.
percentile_5th = simulated_portfolios.cumsum().apply(lambda x : np.percentile(x,5),axis=1)percentile_95th = simulated_portfolios.cumsum().apply(lambda x : np.percentile(x,95),axis=1)average_port = simulated_portfolios.cumsum().apply(lambda x : np.mean(x),axis=1)
And here’s the plot.
The bands are the percentiles and the solid line is the average portfolio. As you can see, after 20 days there’s a 5% probability that our portfolio will lose 5.3% (lower band) and there’s a 5% probability that our portfolio return will be greater than 8.3% (upper band). On average, we expect our portfolio to have a 1% return (solid line).
Another kind of analysis we can perform is the probability that our portfolio will outperform a target return as a function of the day in the future.
Since probability is an observable value, we’d like to calculate its standard error too.
If we have a sample probability p (that is, the number of positive occurrences divided by the number N of scenarios), an estimate of its standard error is:
We can use this value, calculated day by day, to show the error bars in our chart.
Let’s assume we have a 2% target. The probability of beating this target is calculated by:
target_return = 0.02target_prob_port = simulated_portfolios.cumsum().apply(      lambda x : np.mean(x > target_return)   ,axis=1)
The size of the error bars is calculated with the standard error formula:
err_bars = np.sqrt(   target_prob_port * (1-target_prob_port) / n_iterations)
Finally, we can plot the results:
As you can see, the probability of overperforming our target return increases with time (that’s obviously true since our portfolio has a positive average return). After 20 days, our portfolio will outperform the 2% target with a 38.0 +/- 3.4% probability.
Finally, we can calculate some statistics related to Sharpe ratio. It’s a very popular performance metric of a portfolio and, for this example, we can calculate the Sharpe ratio of the returns R of a portfolio as follows:
where E(x) is the expected value and Var(x) is the variance.
Each of our 200 portfolio scenarios has its own Sharpe ratio, so we can calculate it for each one of them. Then we can plot a histogram and calculate the average value.
sharpe_indices = simulated_portfolios.apply(   lambda x : np.mean(x)/np.std(x))
The histogram is:
The mean value is:
In this article, I’ve shown you how to perform a simple scenario analysis for a financial portfolio in Python. This approach can be adapted to many problems and scenarios (e.g. increasing volatility, black swans, a crisis of some industries) in order to assess the possible future risk of a portfolio and its ability to grow up.
This tool could be easily mixed with portfolio optimization in order to assess a complete analysis of financial asset allocation.
[1] Gianluca Malato. Portfolio optimization in R using a Genetic Algorithm. The Trading Scientist. https://medium.com/the-trading-scientist/portfolio-optimization-in-r-using-a-genetic-algorithm-8726ec985b6f
[2] Gianluca Malato. The bootstrap. The Swiss army knife of any data scientist. Data Science Reporter. https://medium.com/data-science-reporter/the-bootstrap-the-swiss-army-knife-of-any-data-scientist-acd6e592be13
[3] Binomial distribution page. Wikipedia. https://en.wikipedia.org/wiki/Binomial_distribution
[4] Sharpe ratio page. Wikipedia. https://en.wikipedia.org/wiki/Sharpe_ratio