Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome back to our module on advanced performance and risk-adjusted returns.
In our last few lessons, we've built a strong toolkit of individual metrics. We explored MIRR to get a more realistic rate of return, the Sharpe ratio to measure risk-adjusted returns, and just last time, we delved into Maximum Drawdown (MDD) to understand an investment's largest historical loss.
Today, we'll transition from theory to practice, focusing on the learning outcome: Write Python code to calculate MIRR, Sharpe Ratio, and Maximum Drawdown for an investment. This lesson is a practical coding session where we'll integrate these three powerful metrics into a single, reusable function. As a software engineer, you'll appreciate how we can encapsulate this complex analysis into a clean, callable tool, moving us closer to building a comprehensive analysis model for your real estate deals.
1. Recapping the Metrics
Let's quickly refresh our memory on the three metrics we'll be coding today:
- Modified Internal Rate of Return (MIRR): A variation of IRR that assumes positive cash flows are reinvested at a more realistic "reinvestment rate" (like a savings account rate) and that the initial investment is financed at a "finance rate" (like a mortgage rate). It provides a more conservative and achievable return figure than IRR. We use
numpy.mirrfor this. - Sharpe Ratio: This metric tells us the return of an investment relative to its volatility (risk). A higher Sharpe ratio indicates a better return for the amount of risk taken. The calculation requires a series of periodic returns and a risk-free rate.
- Maximum Drawdown (MDD): As we covered in detail in the last lesson, this is the largest peak-to-trough decline in an investment's value, representing the worst-case loss over a period. We'll use the efficient
pandasimplementation we developed previously.
2. Setting up the Python Environment and Scenario
We'll use pandas and numpy for our calculations. Let's define a hypothetical 10-year investment in a property in Spain. We'll need a few streams of data for our analysis:
- Annual Net Cash Flows: The cash flow generated by the property each year after all expenses and financing.
- Annual Property Values: The market value of the property at the end of each year. This allows us to calculate annual returns and the drawdown.
Here is the setup in Python:
import pandas as pd
import numpy as np
# --- Investment Scenario ---
# A 10-year hold of a rental property in Spain.
initial_investment = 100_000 # Our cash down payment
# Annual pre-tax cash flow after financing
# Let's assume it grows modestly over time
annual_cash_flows = pd.Series(
[4000, 4100, 4200, 4350, 4500, 3500, 3800, 4800, 5000, 5200]
)
# Projected property value at the end of each year
# Simulates market appreciation, a dip, and recovery.
annual_property_values = pd.Series(
[250000, 265000, 280000, 295000, 270000, 260000, 285000, 310000, 330000, 350000]
)
# Projected sale price at the end of Year 10 is the last value in the series.
# The net proceeds would be this minus selling costs and remaining mortgage.
# For simplicity, we'll use this value to calculate final year return.
sale_price = annual_property_values.iloc[-1]
# --- Rates for MIRR and Sharpe Ratio ---
# A realistic finance rate (e.g., mortgage interest)
finance_rate = 0.035 # 3.5%
# A realistic reinvestment rate (e.g., return on a conservative fund or savings)
reinvestment_rate = 0.015 # 1.5%
# Risk-free rate for Sharpe Ratio (e.g., Spanish 10-year bond yield)
risk_free_rate = 0.02 # 2.0%
3. Implementing the Metric Functions
Now, let's create a separate function for each of our key metrics. This modular approach is good practice and makes our code easier to read, test, and maintain.
Maximum Drawdown Function
We'll use the exact function we created in our previous lesson. It's efficient and clean.
def calculate_max_drawdown(value_series: pd.Series) -> float:
"""Calculates the Maximum Drawdown from a time series of investment values."""
running_max = value_series.cummax()
drawdown = (value_series - running_max) / running_max
return drawdown.min()
This chart illustrates what the drawdown series calculated inside our function looks like visually—a series of declines from the last peak. Our function finds the minimum point on this blue line.

Sharpe Ratio Function
Calculating the Sharpe Ratio involves a few steps: getting periodic returns, calculating excess returns, and then applying the formula. The following video provides an excellent walkthrough of this process for stock data, and we will adapt its logic for our annual real estate data.
The Sharpe Ratio: How to Evaluate Investment Performance with Risk in Mind Using Python
Watch this video from the 'Computer Science' channel to see a clear, step-by-step implementation of the Sharpe Ratio in Python. Pay close attention to how they calculate returns and then use the mean and standard deviation to get the final ratio.
Focus on the segments from 03:59 to 10:21. Specifically, note how .pct_change() is used to get returns (we'll do this), how the risk-free rate is subtracted to get 'excess returns', and the final formula that combines the mean, standard deviation, and an annualization factor.
Drawing from the logic in the video and other financial resources, we can create our function. A key difference is the annualization factor. Since our data is already annual, the number of periods is 1, making the sqrt(periods) term equal to 1. For daily stock data, you'd use sqrt(252).
def calculate_sharpe_ratio(value_series: pd.Series, risk_free_rate: float) -> float:
"""
Calculates the annualized Sharpe Ratio from a series of investment values.
Assumes the value_series contains annual data points.
"""
# 1. Calculate annual returns from the value series
annual_returns = value_series.pct_change().dropna()
# 2. Calculate excess returns
excess_returns = annual_returns - risk_free_rate
# 3. Calculate Sharpe Ratio
# Annualization factor is 1 because we already have annual returns.
# For daily data, this would be sqrt(252).
sharpe_ratio = excess_returns.mean() / excess_returns.std()
return sharpe_ratio
4. Creating the Integrated Analysis Function
Now we'll assemble these components into a single master function. This function will take all our initial data and return a dictionary containing the calculated metrics. This is the kind of reusable tool that becomes invaluable when comparing multiple properties.
def analyze_investment_performance(
initial_investment: float,
cash_flows: pd.Series,
value_series: pd.Series,
finance_rate: float,
reinvestment_rate: float,
risk_free_rate: float,
) -> dict:
"""
Performs a comprehensive risk and return analysis on a real estate investment.
Returns:
A dictionary with key performance metrics.
"""
# --- 1. MIRR Calculation ---
# Create the full cash flow stream for MIRR: [-initial_investment, cf1, cf2, ..., cfn + sale_price]
# For MIRR, we consider the final cash flow to be the regular CF for that year
# plus the net proceeds from the sale. We simplify 'net proceeds' to be our cash-on-cash return.
# The value of our equity is the property value minus remaining debt.
# A more detailed model would have a mortgage amortization schedule.
# For this exercise, we'll consider the "value" to be our initial cash investment.
# The final cash flow is the last period's CF + initial investment back.
# A better approach for a full model is to use Total Equity.
# Let's define the cashflow stream for MIRR as our initial outlay and subsequent annual cashflows
# with the final cashflow including the return OF our principal.
final_cash_flow = cash_flows.iloc[-1] + initial_investment
mirr_cash_flows = list(cash_flows.iloc[:-1])
mirr_cash_flows.append(final_cash_flow)
mirr_cash_flows.insert(0, -initial_investment)
mrr = np.mirr(mirr_cash_flows, finance_rate=finance_rate, reinvest_rate=reinvestment_rate)
# --- 2. Maximum Drawdown Calculation ---
# We calculate MDD on the total property value, as it reflects market risk.
mdd = calculate_max_drawdown(value_series)
# --- 3. Sharpe Ratio Calculation ---
# Sharpe Ratio is also calculated on the total property value series.
sharpe = calculate_sharpe_ratio(value_series, risk_free_rate)
# --- 4. Assemble Results ---
results = {
"MIRR": mrr,
"Maximum Drawdown": mdd,
"Sharpe Ratio": sharpe,
}
return results
# --- Run the Analysis ---
performance_metrics = analyze_investment_performance(
initial_investment=initial_investment,
cash_flows=annual_cash_flows,
value_series=annual_property_values,
finance_rate=finance_rate,
reinvestment_rate=reinvestment_rate,
risk_free_rate=risk_free_rate,
)
# --- Print Results ---
print("--- Investment Performance Analysis ---")
print(f"Modified Internal Rate of Return (MIRR): {performance_metrics['MIRR']:.2%}")
print(f"Maximum Drawdown (MDD): {performance_metrics['Maximum Drawdown']:.2%}")
print(f"Sharpe Ratio: {performance_metrics['Sharpe Ratio']:.2f}")
Interpreting the Output
When you run this code, you'll get a summary like this (exact numbers depend on the input data):
- MIRR: This gives you a single percentage representing the project's compound annual rate of return, assuming realistic reinvestment of cash flows. A result of
8.59%means your investment is effectively growing at that rate annually. - Maximum Drawdown: A result of
-11.86%means that at the worst point during the holding period, the property's value dropped by nearly 12% from its previous peak. This tells you about the paper losses you would have had to endure. - Sharpe Ratio: A value like
1.59is generally considered good (typically > 1 is acceptable, > 2 is very good). It suggests that the investment provided solid returns for the amount of volatility (risk) experienced.
Test your understanding!
You are analyzing a new, more volatile property. The initial_investment is €150,000.
Use the following data:
new_values = pd.Series([300000, 350000, 400000, 320000, 340000, 420000])new_cash_flows = pd.Series([5000, 5500, 6000, 4000, 4500, 6500])
- Run this new property through the
analyze_investment_performancefunction we just built. Use the same finance, reinvestment, and risk-free rates. - Based on the Maximum Drawdown and Sharpe Ratio, how does the risk profile of this new property compare to the first one we analyzed?
Show answer
-
Code to run the analysis:
new_values = pd.Series([300000, 350000, 400000, 320000, 340000, 420000]) new_cash_flows = pd.Series([5000, 5500, 6000, 4000, 4500, 6500]) new_metrics = analyze_investment_performance( initial_investment=150000, cash_flows=new_cash_flows, value_series=new_values, finance_rate=0.035, reinvestment_rate=0.015, risk_free_rate=0.02, ) print("\n--- New Property Analysis ---") print(f"MIRR: {new_metrics['MIRR']:.2%}") print(f"Maximum Drawdown: {new_metrics['Maximum Drawdown']:.2%}") print(f"Sharpe Ratio: {new_metrics['Sharpe Ratio']:.2f}")Expected Output:
--- New Property Analysis --- MIRR: 5.75% Maximum Drawdown: -20.00% Sharpe Ratio: 0.76 -
Risk Profile Comparison:
- Maximum Drawdown: The new property has an MDD of -20.00%, which is significantly larger than the first property's MDD of -11.86%. This indicates it is a much more volatile investment and experienced a far deeper decline from its peak.
- Sharpe Ratio: The Sharpe Ratio of 0.76 is below 1, suggesting that the returns are not as strong relative to the risk (volatility) taken. The first property's Sharpe Ratio of 1.59 was much better.
Conclusion: This new property is riskier. It has higher volatility and has demonstrated a larger potential for significant paper losses, and it doesn't compensate for this extra risk with higher risk-adjusted returns.
Conclusion
Congratulations! You have successfully built a powerful, integrated Python script for advanced investment analysis. By structuring our code into modular functions and then combining them, we've created a tool that can quickly generate key performance indicators for any real estate deal.
Key Takeaways:
- Integration is Power: Combining MIRR, Sharpe Ratio, and MDD gives a multi-faceted view of an investment's return, risk-adjusted return, and downside risk.
- Data Structure Matters: Separating cash flows from asset values allows for correct calculation of different metrics (e.g., MIRR on cash flows, Sharpe/MDD on values).
- Context is Key for Formulas: We adapted the Sharpe Ratio calculation for annual data, highlighting the importance of understanding the assumptions behind the formulas you implement.
- Modular Code is Reusable Code: Our function-based approach allows you to easily apply this analysis to any number of potential investments.
Preview of the Next Lesson:
This concludes our module on advanced performance metrics. We now have a robust way to evaluate the potential of an investment. In our next module, "Risk Modeling and Scenario Analysis," we'll shift our focus from performance to a different type of risk: the risk of default. The first lesson will be "Calculate debt service coverage ratio (DSCR) and interpret lender requirements." This is a critical metric that lenders use to decide whether to grant a mortgage, and understanding it is essential for securing financing.