Black–Scholes Applied to Corporate Liabilities
This take-home exercise evaluates your ability to reason about financial models beyond textbook usage.
You will build a simple structural credit model, apply it to real firm data, diagnose where it fails, and implement a minimal, principled improvement supported by empirical evidence.
This is not a test of formula memorization or software scale. We care most about:
- Modeling judgment (the art of knowing when to bend the rules)
- Numerical discipline
- Empirical reasoning
- Clarity of technical communication
You are asked to:
- Implement a baseline structural credit model (Merton, 1974) in which a firm's equity is modeled as a call option on its assets.
- Calibrate unobservable firm asset value and asset volatility using observable equity prices, equity volatility, debt, and risk-free rates.
- Apply the model to real firms and identify systematic weaknesses in its behavior.
- Propose and implement one minimal, well-justified model improvement.
- Demonstrate quantitatively that the improved model performs better than the baseline under a clearly defined evaluation criterion.
- Clearly document assumptions, methodology, results, and limitations in a concise technical report.
At a debt maturity date
- If firm assets
$V_T > D$ , debt holders are paid in full and equity holders receive$V_T - D$ . - If
$V_T \le D$ , the firm defaults and equity holders receive nothing.
The equity payoff is therefore:
This payoff is identical to a European call option on firm assets with strike equal to the debt face value. This observation underlies the Merton (1974) structural credit model.
The Merton model assumes that firm asset value
where:
-
$r$ is the risk-free rate (assumed constant) -
$\sigma_V$ is the asset volatility (assumed constant) -
$dW_t$ is a Wiener process
Under the risk-neutral measure, the asset value at maturity
where
Equity is valued as a European call option on firm assets using the Black-Scholes formula:
where:
-
$E_t$ is the market value of equity at time$t$ -
$D$ is the face value of debt -
$\Phi(\cdot)$ is the standard normal cumulative distribution function $d_1 = \frac{\ln(V_t/D) + (r + \sigma_V^2/2)(T-t)}{\sigma_V \sqrt{T-t}}$ $d_2 = d_1 - \sigma_V \sqrt{T-t}$
The unobservable parameters
-
Equity value equation:
$E_t = \text{BlackScholes}(V_t, D, T-t, r, \sigma_V)$ -
Equity volatility equation:
$$\sigma_E E_t = \frac{\partial E}{\partial V} \sigma_V V_t$$ where
$\frac{\partial E}{\partial V} = \Phi(d_1)$ is the option delta.
This system is solved iteratively using numerical methods (e.g., scipy.optimize.fsolve).
Distance-to-default (DD) measures how many standard deviations the expected asset value is above the default threshold:
where:
-
$E[V_T] = V_0 e^{rT}$ (expected asset value at maturity) -
$\text{std}(V_T) = V_0 e^{rT} \sqrt{e^{\sigma_V^2 T} - 1}$ (standard deviation)
A higher DD indicates lower default risk.
The risk-neutral default probability is the probability that
where
-
Create a conda environment (recommended):
conda create -n quant_takehome python=3.9 conda activate quant_takehome
-
Install dependencies:
pip install -r requirements.txt
This installs:
numpy,scipy,pandas(numerical computing)matplotlib,seaborn(visualization)jupyter(optional, for notebooks)yfinance,fredapi(for fetching real data)
All required data is already provided in the data/ directory:
data/synthetic/- Synthetic data with known parameters (for validation)data/real/- Real firm data from financial APIs
Note: Data generation scripts are available in baseline/ with DELETE_ prefix for reference only. You do not need to run them.
You can test your setup by running the starter code:
python -m naive_modelThis will show TODO messages until you complete the implementation.
import pandas as pd
# Load equity prices
equity = pd.read_csv('data/real/equity_prices.csv', parse_dates=['date'])
# Load debt (quarterly)
debt = pd.read_csv('data/real/debt_quarterly.csv', parse_dates=['date'])
# Load risk-free rates
rf = pd.read_csv('data/real/risk_free.csv', parse_dates=['date'])
# Explore the data
print(equity.head())
print(f"Firms: {equity['firm_id'].unique()}")
print(f"Date range: {equity['date'].min()} to {equity['date'].max()}")See DATA.md for detailed data documentation.
Example: See examples/data_alignment_example.py for how to align quarterly debt with daily equity data.
The baseline model is the Merton structural credit model, with the following assumptions:
- Firm asset value follows a geometric Brownian motion.
- Default occurs only at maturity
$T$ if asset value is below debt. - Equity is treated as a European call option on firm assets.
- Interest rates and asset volatility are constant.
- The firm's capital structure is represented by a single debt face-value proxy.
For each firm and evaluation date, your baseline implementation must produce:
- Estimated firm asset value
$V_t$ - Estimated firm asset volatility
$\sigma_V$ - Distance-to-default (DD)
- Default probability (PD), or an equivalent implied credit-risk measure
After implementing and evaluating the baseline model, you must identify at least one systematic failure or vulnerability. (Because no model is perfect... except maybe this one in our dreams)
Examples include (but are not limited to):
- Default only occurring at maturity
- Excessive sensitivity to maturity or volatility assumptions
- Poor behavior for highly leveraged or distressed firms
- Numerical instability or implausible risk dynamics
You must then:
- Propose one primary model improvement
- Implement it
- Demonstrate empirically that it improves performance relative to the baseline
Your solution should be organized as follows:
naive_model/ # Your baseline Merton model implementation
├── __init__.py
├── __main__.py # Entry point: python -m naive_model
├── model.py # Merton model implementation
├── calibration.py # Asset value/volatility calibration
└── risk_measures.py # DD, PD calculations
improved/ # Your improved model implementation
# (Copy from naive_model/ and modify)
├── __init__.py
├── __main__.py # Entry point: python -m improved
├── model.py # Improved model implementation
├── calibration.py # Calibration for improved model
└── risk_measures.py # Risk measures for improved model
Starter files are provided in naive_model/ directory. You should:
- Complete the baseline implementation in
naive_model/ - Copy
naive_model/toimproved/and modify it with your improvement - Ensure both can be run directly:
python -m naive_modelandpython -m improved
- The improvement must be minimal (modify one core assumption).
- The improvement must be well-justified.
- Complexity is not rewarded. (Keep it simple and stupid)
- Both implementations should produce comparable outputs (asset value, volatility, DD, PD)
You must define what “better performance” means and provide evidence.
Acceptable evaluation criteria include:
- Improved alignment with provided benchmark credit signals (if used)
- Improved cross-sectional risk ranking across firms
- Improved time-series stability of risk measures
- Reduction of implausible or unstable behavior
- Improved out-of-sample or rolling-window performance
Your evaluation must include:
- A clearly defined metric
- A comparison between baseline and improved models
- Economic interpretation of results
All required data is provided in the data/ directory:
data/
├── equity_prices.csv
├── equity_vol.csv
├── debt_quarterly.csv
└── risk_free.csv
- Equity data is daily.
- Debt is reported quarterly and must be aligned appropriately.
- A single debt proxy is used by design.
- Risk-free rates are provided as a constant or a time series.
Precise definitions, units, and alignment rules are specified in DATA.md.
- Use only the provided data unless explicitly justified.
- No look-ahead bias or future information leakage.
- All simplifying assumptions must be stated and defended.
The baseline/ directory contains a reference implementation for validation purposes only:
baseline/
├── DELETE_synthetic_test.py # Data generator (for reference only)
└── DELETE_generate_real_data.py # Data fetcher (for reference only)
You may organize your solution as you see fit. A typical structure might include:
src/ # model and calibration code
evaluation/ # metrics and comparisons
outputs/ # tables and figures
report/ # final report
Clarity and reproducibility matter more than structure.
Your submission must include:
- Fully reproducible code
- Clear instructions or a single entrypoint to run experiments
- Reasonable runtime on a standard laptop
The report should include:
- Model formulation and assumptions
- Calibration methodology
- Empirical setup and data description
- Diagnosis of baseline model weaknesses
- Description of the proposed improvement
- Quantitative results and evaluation
- Limitations and possible extensions
- Tables and/or figures supporting your conclusions
- Clear labeling and economic interpretation
New to this? See WALKTHROUGH.md for a step-by-step guide
- Fork this repository.
- Clone your fork locally.
- Create a Python environment (see Setup section above).
- Install dependencies:
pip install -r requirements.txt - Complete the baseline in
naive_model/(see WALKTHROUGH.md) - Implement your improvement in
improved/
- Fork this repository.
- Clone your fork locally.
- Create a Python environment (virtualenv or conda).
- Install dependencies listed in
requirements.txt. - Implement the baseline model in
naive_model/(see WALKTHROUGH.md).
- Push all code, report, and output artifacts to your fork.
- Provide:
- A link to your fork, or
- A compressed archive of the repository
- Include the exact command(s) required to reproduce your results.
Submissions will be evaluated on:
- Correctness and numerical discipline
- Quality of model diagnosis
- Soundness of the proposed improvement
- Strength of empirical evidence
- Clarity of technical communication
Thoughtful, well-reasoned solutions are valued over complexity.
- Merton, R. (1974). On the Pricing of Corporate Debt
- Black, F., & Cox, J. (1976). Valuing Corporate Securities
- Crosbie, P., & Bohn, J. (KMV). Modeling Default Risk