Portfolio Analysis in Python: The Impact of the Ukraine War on European and American Companies

Ogulcan Ertunc
DataDrivenInvestor
Published in
12 min readJul 27, 2023

--

Photo by Jamie Street on Unsplash

On February 24, 2022, an unfortunate and unexpected conflict began between Ukraine and Russia. This article presents an exploration of the financial implications of this war on a specific portfolio comprised of select European and American companies. The timeline of interest spans from the onset of the war to roughly 3.5 months after.

The Pretext

To gauge the impact of the war on media outlets, I utilized the Global Database of Events, Language, and Tone (GDELT). My analysis of news frequency from January to June 2022 was guided by the following search terms related to the “Ukraine War”: “Ukrainian war”, “who is winning the war in Ukraine”, “russo-ukrainian war map live”, “Ukraine breaking news today”. The chart reveals a significant peak in news coverage during April, indicating a strong correlation between the war and news sources.

My GDELT-based news intensity graph

Given this apparent correlation, it’s reasonable to conjecture similar effects on financial performance. To examine this, I constructed two portfolios: one consisting of the nine largest European companies and another comprising four major American companies. These portfolios were analyzed from January 2021 through June 14, 2022.

Data Preparation

The datasets were easily prepared using yfinance, with the following tickers representing the selected companies:

European Portfolio: [‘MC.PA’, ‘NVO’, ‘ASML’, ‘OR.PA’, ‘ACN’, ‘SAP’, ‘CDI.PA’, ‘PRX.AS’, ‘RMS.PA’]. American Portfolio: [‘AAPL’, ‘MSFT’, ‘GOOG’, ‘AMZN’].

Given the need to normalize data for the Sharpe Ratio calculation, I chose to use logarithmic returns, which are straightforward to calculate for date-based data.

tickers = ['MC.PA', 'NVO', 'ASML', 'OR.PA', 'ACN', 'SAP', 'CDI.PA', 'PRX.AS', 'RMS.PA']
tickers_usa = ['AAPL', 'MSFT', 'GOOG', 'AMZN']
# time periods
start_date = '2021-01-01'
end_date_1 = '2022-02-24'
end_date_2 = '2022-06-15'

# Download historical stock data for the given tickers and time periods
data_1 = yf.download(tickers, start=start_date, end=end_date_1)['Adj Close']
data_2 = yf.download(tickers, start=start_date, end=end_date_2)['Adj Close']

plt.figure(figsize=(12, 8))
for ticker in data_2.columns:
plt.plot(data_2.index, data_2[ticker], label=ticker)

plt.xlabel('Date')
plt.ylabel('Adjusted Close Price')
plt.title('Historical Stock Prices of EU Companies')
plt.legend(loc='upper left')

# Adding energy firm tickers as labels on the lines
for ticker in data_2.columns:
last_price = data_2[ticker].dropna().iloc[-1]
plt.text(data_2.index[-1], last_price, ticker, ha='left', va='center')

plt.grid(True)
plt.show()
Share Prices graph of European companies in the specified time period
Share Prices graph of American companies in the specified time period
returns_df = data_2.pct_change().dropna()
returns_df

Portfolio Risk and Return: The Sharpe Ratio

The Sharpe Ratio, named after Nobel laureate William F. Sharpe, quantifies the risk-adjusted return of an investment. It offers an insightful measure for comparing an investment to a risk-free asset.

The formula is:

Sharpe Ratio Formula

Sharpe Ratio = (Expected portfolio return — Risk-free rate) / Portfolio standard deviation

Here, the expected portfolio return is the investor’s projected earnings, the risk-free rate corresponds to the return expected from a virtually riskless investment (e.g., a U.S. Treasury bond), and the portfolio standard deviation measures the investment’s volatility. A high Sharpe Ratio indicates more return per unit of risk, while a negative value suggests that a risk-less investment outperforms the given investment.

European Companies based Portfolio Sharpe Ratio Graph before the war
European Companies based Portfolio Sharpe Ratio Graph full-time period
American Companies based Portfolio Sharpe Ratio Graph full-time period

Applying the Sharpe Ratio to the selected portfolios involved simulating various stock combinations and maintaining respective Sharpe Ratios for 9000 European and 4000 American portfolios. Following that, I used .max() and .argmax() functions to identify portfolios with maximum Sharpe Ratios.

print("Max sharpe ratio for the first date in the array {}".format(sharpe_arr_1.max()))
print("Its location in the array: {}".format(sharpe_arr_1.argmax()) )
>>Max sharpe ratio for the first date in the array 1.4120737157303485
>>Its location in the array: 4442

print("Max sharpe ratio for the second date in the array {}".format(sharpe_arr_2.max()))
print("Its location in the array: {}".format(sharpe_arr_2.argmax()) )
>>Max sharpe ratio for the second date in the array 0.7228172454931211
>>Max sharpe ratio for the second date in the array 0.7228172454931211

Portfolio Optimizations

In this section, I explore hypothetical scenarios where an investment of 100,000 dollars was made in the aforementioned stocks at the beginning of 2021, using various optimization methods.

def portfolio_returns(expected_returns_1, expected_returns_2, weight_1, weight_2):
# Calculate portfolio returns using the optimized weights
portfolio_returns_1 = np.sum(expected_returns_1 * weight_1)
portfolio_returns_2 = np.sum(expected_returns_2 * weight_2)

# Step 2: Calculate income or profit generated by the portfolio for each specific date

# Assuming you have the initial investment amount
initial_investment = 100000 # Example: 100,000 USD

# Calculate the final portfolio value for each specific date
final_portfolio_value_1 = initial_investment * (1 + portfolio_returns_1)
final_portfolio_value_2 = initial_investment * (1 + portfolio_returns_2)

# Calculate the income or profit for each specific date
income_1 = final_portfolio_value_1 - initial_investment
income_2 = final_portfolio_value_2 - initial_investment

# Print the results
print(f"Optimized Weights for 15th March 2022: {weight_1}")
print(f"Portfolio Return for 15th March 2022: {portfolio_returns_1:.4f}")
print(f"Income for 15th March 2022: {income_1:.2f} USD")

print(f"\nOptimized Weights for 15th June 2022: {weight_2}")
print(f"Portfolio Return for 15th June 2022: {portfolio_returns_2:.4f}")
print(f"Income for 15th June 2022: {income_2:.2f} USD")

Sharpe Ratio Optimization

Using optimized values from Sharpe Ratio calculations, I can compare potential returns for the selected portfolios. The analysis reveals both portfolios would have suffered losses, but the larger damage to European companies indicates a more significant economic impact of the war.

portfolio_returns(expected_returns_1, expected_returns_2,weights_sharpe_1,weights_sharpe_2)
Portfolio Outcome with Sharpe Ratio Weights

Markowitz Model (Mean-Variance Optimization)

Proposed by Harry Markowitz in 1952, the Markowitz Model or Modern Portfolio Theory (MPT) attempts to maximize portfolio return for a given amount of risk. When applied to data, the model was more successful than the Sharpe Ratio, particularly for the American portfolio, which managed to secure minor profits. However, war’s impact significantly reduced the returns during the peak conflict period.

The solution to the Markowitz model is the efficient frontier, which represents the set of optimal portfolios that offer the highest expected return for a defined level of risk or the lowest risk for a given level of expected return.

Both the Sharpe Ratio and the Markowitz model are used in portfolio optimization to achieve the best possible returns for a given level of risk, but they focus on slightly different aspects, leading to potentially different optimized weights for portfolio assets.

# Markowitz Model (Mean-Variance Optimization)
def mean_variance_optimization(returns,days):
num_assets = len(returns.columns)
weights = np.random.random(num_assets)
weights /= np.sum(weights)

def portfolio_variance(weights):
return np.dot(weights.T, np.dot(returns.cov() * days, weights))

def portfolio_return(weights):
return np.sum(returns.mean() * weights) * days

def objective_function(weights):
return portfolio_variance(weights)

constraints = ({'type': 'eq', 'fun': lambda x: np.sum(x) - 1})
bounds = tuple((0, 1) for _ in range(num_assets))
optimized_weights = minimize(objective_function, weights, method='SLSQP', bounds=bounds, constraints=constraints)

return optimized_weights.x

optimized_weights_markowitz_1 = mean_variance_optimization(returns_df.loc[:target_date_1],297)
optimized_weights_markowitz_2 = mean_variance_optimization(returns_df.loc[:target_date_2],374)

portfolio_returns(expected_returns_1, expected_returns_2,optimized_weights_markowitz_1,optimized_weights_markowitz_2)
Markowitz Model Output with European Companies’ Portfolio
Markowitz Model Output with American Companies’ Portfolio

Black-Litterman Model

The Black-Litterman Model is an approach to portfolio optimization that was developed by Fischer Black and Robert Litterman of Goldman Sachs in 1992. It is a modification of the traditional Markowitz portfolio theory and addresses some of its limitations.

Markowitz’s Modern Portfolio Theory relies heavily on historical returns to determine the optimal asset weights in a portfolio. In practice, this can lead to extreme weights and a lack of diversification, as it’s often sensitive to the estimated returns. The Black-Litterman model was developed to address these issues and create more intuitive, diversified portfolios.

It was particularly effective in analysis before the war for European companies, while not as effective for American companies. Post-war, however, it predicted a significant loss in Europe and a gain of around 5% in America.

tau = 0.05  # Tau (a scalar parameter) adjusts the impact of investor views

# Black-Litterman Optimization
def black_litterman_optimization(returns, P, Q, Omega, tau, days):
tau_cov = tau * returns.cov()
inv_tau_cov = np.linalg.inv(tau_cov)

P_T = P.T
M_inverse = np.linalg.inv(np.dot(P, np.dot(inv_tau_cov, P_T)) + np.linalg.inv(Omega))

excess_returns = np.dot(inv_tau_cov, returns.mean() * days) + np.dot(np.dot(P_T, M_inverse), (Q - np.dot(P, returns.mean() * days)))

num_assets = len(returns.columns)
weights = excess_returns / tau

return weights

# Convert views into the 'P' matrix and 'Q' vector for each target date
P_1 = np.eye(len(returns_df.columns))
Q_1 = investor_view_1
Omega_1 = np.diag(confidence_1 ** 2)

P_2 = np.eye(len(returns_df.columns))
Q_2 = investor_view_2
Omega_2 = np.diag(confidence_2 ** 2)

# Optimize portfolio using Black-Litterman for target date 15th March 2022
optimized_weights_bl_1 = black_litterman_optimization(returns_df.loc[:target_date_1], P_1, Q_1, Omega_1, tau, 297)

# Optimize portfolio using Black-Litterman for target date 15th June 2022
optimized_weights_bl_2 = black_litterman_optimization(returns_df.loc[:target_date_2], P_2, Q_2, Omega_2, tau, 374)

# Normalize the optimized weights to ensure they sum up to 1
optimized_weights_bl_1 /= np.sum(optimized_weights_bl_1)
optimized_weights_bl_2 /= np.sum(optimized_weights_bl_2)

portfolio_returns(expected_returns_1, expected_returns_2,optimized_weights_bl_1,optimized_weights_bl_2)
Black-Litterman Model Output with European Companies’ Portfolio
Black-Litterman Model Output with American Companies’ Portfolio

Mean-CVaR Optimization

Mean-CVaR Optimization, also known as Expected Shortfall Optimization, is a method of portfolio optimization that focuses on managing and minimizing extreme risks.

CVaR stands for Conditional Value at Risk (also known as Expected Shortfall), which is a risk measure that looks at the losses that could occur in the worst-case scenarios. Unlike the more common Value at Risk (VaR) measure, which just gives the maximum loss at a certain confidence level, CVaR gives the expected loss given that the loss is beyond the VaR. This makes it a more sensitive measure to extreme losses and tail risks.

When applied to data, the model performed cautiously, managing to profit before the war from the European portfolio but failing to evade the effects of the war.

# Mean-CVaR Optimization

def portfolio_cvar(weights, returns, confidence_level, days):
portfolio_return = np.sum(returns.mean() * weights) * days
portfolio_loss = np.dot(returns, weights) - portfolio_return
cvar = -np.percentile(portfolio_loss, confidence_level)
return cvar

def mean_cvar_optimization(returns, confidence_level, days):
num_assets = len(returns.columns)
weights = np.random.random(num_assets)
weights /= np.sum(weights)

def objective_function(weights):
return -np.sum(returns.mean() * weights) * days

constraints = ({'type': 'eq', 'fun': lambda x: np.sum(x) - 1})
bounds = tuple((0, 1) for _ in range(num_assets))

# Set the CVaR constraint
cvar_constraint = {'type': 'ineq', 'fun': lambda x: portfolio_cvar(x, returns, confidence_level, days)}

# Combine the objective and constraints
constraints = [constraints, cvar_constraint]

# Minimize the negative portfolio return (maximize portfolio return) subject to the CVaR constraint
optimized_weights = minimize(objective_function, weights, method='SLSQP', bounds=bounds, constraints=constraints)

return optimized_weights.x

# Set the confidence level for CVaR risk measure (e.g., 95%)
confidence_level = 5

# Optimize portfolio using Mean-CVaR for target date 15th March 2022 and 15th June 2022
optimized_weights_meancvar_1 = mean_cvar_optimization(returns_df.loc[:target_date_1], confidence_level, 297)
optimized_weights_meancvar_2 = mean_cvar_optimization(returns_df.loc[:target_date_2], confidence_level, 374)
Mean-CVaR Model Output with European Companies’ Portfolio
Mean-CVaR Model Output with American Companies’ Portfolio

In summary, each portfolio optimization method offered unique perspectives on potential investment outcomes. While all predictions are fundamentally historical and cannot guarantee future results, such analysis can provide useful insights into the expected performance of specific investment strategies, especially in light of geopolitically charged events such as the Ukraine war. So, I thought that if I add the sentiment values of news sources as portfolio weight, maybe I can reduce the risk of loss in a safer range, and in addition, I wanted to combine the results of the Black-Litterman Model, which is the most successful in terms of income, with sentiment scores.

A Hybrid Approach to Portfolio Optimization: Black-Litterman Model and GDELT Sentiment Analysis

Photo by Chris Liverani on Unsplash

Given the inherent difficulties of traditional portfolio optimization models, a new hybrid strategy combining the Black-Litterman model with sentiment analysis from the Global Events, Language and Tone Database (GDELT) project might make sense.

This hybrid approach aims to create a more sophisticated understanding of market dynamics by allowing investors to build more robust and flexible portfolios, especially during times of geopolitical unrest.

If you are not familiar with GDELT
GDELT Sentiment Analysis: Sentiment analysis derived from the GDELT project provides a novel approach to factor in global societal events and their potential impacts on the market. GDELT analyzes news from across the globe in over 100 languages and assigns a sentiment score. This score measures the positive or negative tone associated with various geopolitical events.

By merging these elements, the hybrid model looks to capitalize on the strength of the Black-Litterman model’s tailored investment strategy while enhancing its capabilities with the incorporation of real-time global sentiment analysis.

It’s important to note that while this approach offers a unique perspective on portfolio optimization, it is not a foolproof solution.

from gdeltdoc import GdeltDoc, Filters

def gdelt_scrapper(keywords, start_date, end_date):
key_word = keywords
start = start_date
end = end_date
f = Filters(
keyword=key_word,
start_date=start,
end_date=end,
theme = "ECON_STOCKMARKET"
)
gd = GdeltDoc()
# get sentiment changes from gdelt with TimelineTone
tone = gd.timeline_search("timelinetone", f)
tone['datetime'] = pd.to_datetime(tone['datetime'])
return tone

start_date = '2021-01-01'
end_date = '2022-06-15'
list_of_firms = ["MC.PA", "Novo Nordisk",'ASML Holding NV', 'Loreal', 'Accenture', 'SAP SE', 'Christian Dior SE', 'PRX.AS', 'RMS.PA']

tone_m = pd.DataFrame()
for i in list_of_firms:
tone = gdelt_scrapper(i, start_date, end_date)
tone.rename(columns={"Average Tone": f"Average Tone {i}"}, inplace=True)
if tone_m.shape[0] == 0:
tone_m = tone
else:
tone_m=tone_m.merge(tone, how='left', on="datetime")
print(f"{i} is added...")

tone = tone_m.set_index("datetime")
tone_m_02_24 = tone[tone.index <= "2022-02-24"]
tone_m_02_24_mean = tone_m_02_24.mean()
tone_m_mean = tone.mean()
# Empty list to store the new weights
new_weights_1 = []
new_weights_2 = []

# Combining the weights
for i in range(len(optimized_weights_bl_1)):
bl_weight = optimized_weights_bl_1[i]
sentiment_score = sentiment_scores_1[i]

# Here I'm simply taking a weighted sum of the bl weights and the sentiment scores
new_weight = 0.5 * bl_weight + 0.5 * sentiment_score
new_weights_1.append(new_weight)

for i in range(len(optimized_weights_bl_2)):
bl_weight = optimized_weights_bl_2[i]
sentiment_score = sentiment_scores_2[i]

# Here I'm simply taking a weighted sum of the bl weights and the sentiment scores
new_weight = 0.5 * bl_weight + 0.5 * sentiment_score
new_weights_2.append(new_weight)

# Convert the list of new weights to a DataFrame
weights_df_1 = pd.DataFrame({'Stock': sentiment_scores_1, 'Weight': new_weights_1})
weights_df_2 = pd.DataFrame({'Stock': sentiment_scores_2, 'Weight': new_weights_2})

portfolio_returns(expected_returns_1, expected_returns_2,list(new_hybrid_1),list(new_hybrid_2))
portfolio_returns(expected_returns_1, expected_returns_2,optimized_weights_bl_1,optimized_weights_bl_2)
Hybrid Model Output with European Companies’ Portfolio before the war
Hybrid Model Output with European Companies’ Portfolio Full-time

Afterward, I checked the effect of the 100k dollar portfolio I made after each optimization and realized that the effect of the sentiment model was effective in reducing the post-war damage. In order to increase the success of the model in the future, a weighted average can be calculated by using the news numbers from which the sentiment scores are obtained as weight.

Conclusion

Photo by Jordy Meow on Unsplash

The Ukrainian War had a marked impact on both European and American companies. In analysis, I saw how conflict affects the performance of selected portfolios and how different investment strategies can alter results.

However, it is very important to note that investment portfolios are not only affected by political events. Numerous factors such as technological advances, consumer behavior changes, and even environmental crises can have significant effects on investment results. In this context, adding something simple and effective like sentiment to your investment can be another metric you can consider depending on the type and time of your investment.

In the investing world, it is always essential to keep up with current events, conduct extensive research, and frequently re-evaluate investment strategies to adapt to the ever-evolving landscape.

After all, the goal of portfolio optimization is not only to maximize profits but also to reduce risks in times of uncertainty.

No matter the circumstances, keep in mind that investing always involves a degree of risk. Understanding how these risks can be mitigated through different investment strategies and portfolio optimization models, as explored in this article, can significantly enhance the potential for successful investing in the face of geopolitical unrest.

Photo by Benjamin Wedemeyer on Unsplash

Remember, as Robert Arnott said,

“In investing, what is comfortable is rarely profitable.” So, embrace the discomfort, stay informed, stay flexible, and keep your investments diversified.

Photo by Elevate on Unsplash

References:

Subscribe to DDIntel Here.

DDIntel captures the more notable pieces from our main site and our popular DDI Medium publication. Check us out for more insightful work from our community.

Register on AItoolverse (alpha) to get 50 DDINs

Support DDI AI Art Series: https://heartq.net/collections/ddi-ai-art-series

Join our network here: https://datadriveninvestor.com/collaborate

Follow us on LinkedIn, Twitter, YouTube, and Facebook.

--

--

I’m an IT Consultant,graduated Data Analytics. I’m a Data Enthusiast 💻 😃 passionate about learning and working with new tech. https://github.com/ogulcanertunc