Why HJM Forward-Rate Averages Differ from Initial Forwards
Summary
The document describes a simulation question about the Heath–Jarrow–Morton interest-rate framework. The author simulates forward rates with constant volatility and antithetic normal draws, then compares averages of simulated rates with corresponding values on the initial forward curve. The observed mismatch prompts the mistaken expectation that forward rates should retain their initial values on average.
The resolution is that forward rates under the money-market measure are not generally martingales. Their simulated averages need not equal the initial forward rates; the difference is related to a convexity adjustment between the money-market and forward measures. The author reports that testing bond prices as martingales satisfied the expected condition. The exchange answer gives the measure-based explanation, but the document does not provide a full derivation or quantify the adjustment. It also notes that simulation error grows at higher volatility.
Key ideas
- Forward rates in HJM need not be martingales under the money-market measure.
- A simulated path average and an initial forward rate can differ because they are associated with different measures.
- The difference is analogous to a convexity adjustment.
- Bond prices, rather than forward rates, were reported to satisfy the tested martingale condition.
Tags
Full text
# Convergence of HJM model with constant volatility
# Convergence of HJM model with constant volatility
In HJM is expected that for each step in time the mean of the tenors will be equal the corresponding tenors in the initial forward curve.
I saw a divergence in my model so I give some steps back and try in a simpler model: constant volatility.
But I see the same divergence here. I follow Glasserman to build the code but I cant figure out the error.
Here is a code sample:
```
import numpy as np
def construct_random_numbers(seed_value: int, number_of_simulations: int, number_of_risk_factors: int, number_of_timesteps: int) -> np.array:
np.random.seed(seed_value)
if number_of_simulations % 2 == 1:
raise ValueError("Number of paths must be even")
sn = np.random.standard_normal((int(number_of_simulations / 2), number_of_risk_factors, number_of_timesteps))
sn = sn.astype(np.float32)
sn = np.concatenate((sn, -sn), axis=0)
sn = (sn - sn.mean()) / sn.std()
return sn
if __name__ == "__main__":
number_of_simulations = 10
number_of_timesteps = 300
number_of_tenors = 300
dt = 1/12.0
sqrt_dt = np.sqrt(dt)
vol = 0.1
z = construct_random_numbers(42, number_of_simulations, 2, number_of_timesteps)
initial_curve = [0.13916995924421227, 0.13927470149892207, 0.13931937944665784, 0.13954950876573402, 0.1394163953124534, 0.13922719184309001, 0.13940934362894178, 0.13920600960110618, 0.13946900910538873, 0.1393136275348182, 0.1393513763811261, 0.13949331966475423, 0.13923202364314158, 0.1392210281814887, 0.13930238006736542, 0.13946457788510194, 0.13920880539245234, 0.13907752444018895, 0.1393156241141591, 0.1394159140799911, 0.1394807661749739, 0.13931442386379148, 0.13931959733928842, 0.13947613007342557, 0.13917376583003047, 0.1393218879224817, 0.1393797823889925, 0.13940566187212852, 0.13930975933068093, 0.1393641841011706, 0.13911190726465872, 0.13929491515876663, 0.13940609087125547, 0.13926251059657455, 0.13936663842215896, 0.1394054272500803, 0.13932648507991424, 0.13937778637709966, 0.13926446674347154, 0.1393400788720304, 0.13942176161695893, 0.13937643729609686, 0.13930208687304363, 0.13938316164042908, 0.13943220097906367, 0.13915574587214227, 0.1394766474064956, 0.1393271145261319, 0.13940328899332402, 0.13948046469236453, 0.13922357263027696, 0.13928979589975726, 0.13933188513041245, 0.13928236464788757, 0.1395342336575837, 0.13927506279061733, 0.13930518532205133, 0.13938117890316032, 0.1392405250473924, 0.13932117192882437, 0.13939248532612503, 0.13932745395956175, 0.13935140131411003, 0.13930780678875518, 0.13920929296908194, 0.13939289932113394, 0.13953623330716475, 0.13936258016307834, 0.13938326641172477, 0.1394300122755117, 0.13923380695034535, 0.13953099146581296, 0.1393809728796423, 0.13952392568990893, 0.1392874367004476, 0.13954254619225578, 0.13943825554940348, 0.13941604400858526, 0.13938414244543532, 0.13933588713819256, 0.1394807931658474, 0.1393747759869848, 0.13916403302964128, 0.1392490213538293, 0.13934215542478073, 0.13939173989831657, 0.13935508115111003, 0.13948263707897937, 0.13916562981359615, 0.13937509879365054, 0.1391789420774613, 0.13943671122954843, 0.13935129412855582, 0.13940270099720242, 0.13927435170394104, 0.1394886864931658, 0.13946491246640588, 0.1394190944303012, 0.13934778827531366, 0.13941872572852954, 0.13929005074320128, 0.13944629556590207, 0.13952568808943241, 0.13946780105599793, 0.13928777975301926, 0.13939688173172732, 0.13923142584562415, 0.13941797903111758, 0.13940580034128627, 0.13917375836604678, 0.13924456161560878, 0.13933242611032565, 0.13933065402528594, 0.13923979230645814, 0.13939963787293522, 0.13938588736089405, 0.13934131308680217, 0.13928516084998419, 0.13946561071264166, 0.13952628071130885, 0.13931238385161535, 0.13929952330828452, 0.13940986076654122, 0.1393249370620253, 0.13934680027993368, 0.1394347053151725, 0.1391698043912493, 0.1394925702324853, 0.13936800805243293, 0.13931959068554378, 0.13935181658457788, 0.13935698293550206, 0.13918761986965553, 0.13951023382274533, 0.13936487256322708, 0.13940504021304476, 0.1394558949770574, 0.13925862375424397, 0.13940808718990352, 0.13933397589091012, 0.13944541707752167, 0.13914801725796486, 0.13935083056997588, 0.1393004579344216, 0.13940396629506344, 0.13940774441873682, 0.1392899774105178, 0.1392895226135824, 0.13934967346314428, 0.1392710757772685, 0.13944493836954522, 0.13932381927995527, 0.13907004117806784, 0.13932325779433782, 0.1392568856179156, 0.1391976124184539, 0.13923699733617775, 0.13937168084718332, 0.13949360408282482, 0.1393920584473865, 0.13930008966117843, 0.13940633041951708, 0.13931483096897812, 0.1392912799630837, 0.1392453037331767, 0.13918555711932484, 0.13932174762934407, 0.13937654534899202, 0.13935107763299853, 0.13931914330720527, 0.13926616539393033, 0.13942156461916852, 0.13922902680800536, 0.13943834279138437, 0.13926051829810734, 0.1392854964762647, 0.13921406148093846, 0.13931059978531143, 0.1394582712458478, 0.1394123028088444, 0.1394585822112028, 0.1394129542545934, 0.13925037301714696, 0.13932766107756725, 0.13928272008708825, 0.13949379749040391, 0.1392410611353574, 0.13945010976893893, 0.1394500353870109, 0.1395437020201716, 0.13931977134073706, 0.13943197221595802, 0.13958890518365388, 0.13929897583230663, 0.13920723597524678, 0.13917273899660923, 0.13943201120288615, 0.13917242497919416, 0.13932350809470623, 0.1394044713187039, 0.139379257553683, 0.13940328448641842, 0.13938296898558009, 0.13948862658133135, 0.13926550836192614, 0.13929304048615415, 0.13928347405547292, 0.13944375918118626, 0.13945077985883952, 0.13940586253193185, 0.139479089366689, 0.13924293591326237, 0.13952746241115513, 0.13951306386217124, 0.13943430219972458, 0.1395389198908771, 0.13919507767923223, 0.13930829518637375, 0.13928720965393182, 0.13943288793237574, 0.1392557259260398, 0.13943648951493376, 0.13949779361439968, 0.1394302602900084, 0.13938911510236615, 0.13926195266590993, 0.1392760549033875, 0.13948570189052967, 0.13943867490039055, 0.13935683804806226, 0.1393189490454234, 0.13915815921268887, 0.13925365475813156, 0.13955188190871398, 0.13923269444289976, 0.1393796879821221, 0.13946148439731954, 0.13923524773319979, 0.1393792110474781, 0.13945453155375107, 0.13936198993219073, 0.13964758030556668, 0.13919388007411218, 0.1394666128529884, 0.13934345982626037, 0.13943793201548368, 0.13940487071516994, 0.13928742438028902, 0.13938363963924594, 0.13917494985486017, 0.13942106475005714, 0.13939055584659907, 0.13942460601686432, 0.13946655183250872, 0.13939750198402098, 0.13937554894197984, 0.13927263316633387, 0.13928951792363511, 0.13947941346201775, 0.1395043735405706, 0.13933692895636882, 0.13936534027472533, 0.13927027856250063, 0.13947801568452675, 0.13942728168236918, 0.1395548617436464, 0.13927290769228204, 0.1393229392100982, 0.13938388459225257, 0.1394253889537726, 0.13933917323624526, 0.13927344389095464, 0.13944888075289236, 0.13943285353625456, 0.13926911258994365, 0.13949628428011057, 0.13947775987091968, 0.1393741696497248, 0.13936869978623762, 0.1394161457771116, 0.1392745696929744, 0.1393637431479164, 0.1391455016455391, 0.1394483855645503, 0.1392045906777558, 0.1395110819828429, 0.13932772799060336, 0.13933957330180688, 0.13929796660939156, 0.1393319361126078, 0.13950259313451482, 0.1391119591343465, 0.13948843653860882, 0.1393945423481102, 0.1392400378352143, 0.13925320876069508, 0.13952837930272083, 0.13926618931200574, 0.13924540494894042, 0.13947447145423647]
paths = np.zeros(shape=(number_of_simulations, number_of_timesteps + 1, number_of_tenors))
paths[:, 0, :] = initial_curve
for tenor_index in range(number_of_tenors):
for timestep_index in range(1, number_of_timesteps + 1):
if timestep_index <= tenor_index:
T = dt * tenor_index
t = dt * timestep_index
random_numbers = z[:, 0, timestep_index - 1]
drift_integral = 0.5 * (vol ** 2) * (T ** 2 - (T - t) ** 2) * np.ones(shape=(number_of_simulations,))
volatility_multiplied_by_random_numbers = vol * np.sqrt(t) * random_numbers * np.ones(shape=(number_of_simulations,))
paths[:, timestep_index, tenor_index] = paths[:, 0, tenor_index] + drift_integral + volatility_multiplied_by_random_numbers
# running over time the mean in the short rate must match the corresponding tenor from the initial curve
for tenor_index in range(1, number_of_tenors):
diff = np.mean(paths[:, tenor_index, tenor_index]) - paths[0, 0, tenor_index]
print(f"{tenor_index}: {diff}")
print("@")
# running over time the mean in each tenors must match the corresponding tenor in the initial curve
for timestep_index in range(number_of_timesteps + 1):
for tenor_index in range(number_of_tenors):
if timestep_index + tenor_index < number_of_tenors:
diff = np.mean(paths[:, timestep_index, timestep_index + tenor_index]) - paths[0, 0, timestep_index + tenor_index]
print(f"({timestep_index}, {tenor_index}): {diff}")
print("@")
```
Could you help me find out what I'm missing>
Actually this question is already solved. The misunderstanding was that forward curves are not martingales in HJM, and I was testing for this.
Glasserman appointed this misunderstanding to me. in this model Bond prices are martingales, when I tested then the condition was satisfied.
For high values of volatility the error increases, but now everything is matching as expected.
Thanks!
## Answer by dm63 (score 2)
https://quant.stackexchange.com/a/82353
Are you asking why in HJM the simulated path average for some rate is not equal to the forward rate ? So for example the five year rate observed one year from now is such that the path average is say 4% whereas the forward rate is say 3.95% ?
If so this is well known. The path average is in the money market measure whereas the forward rate is in the forward measure. The difference is akin to a convexity adjustment.Shown in full with attribution under the source's licence. Licence: CC BY-SA 4.0 (Stack Exchange)
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.