The log return¶
A price's log return from the one before, r = ln(p / p0): the continuously compounded return between two samples, dimensionless.
Generic over the game's money unit, as crossing_price is: both prices in one unit, which divides out. The sampling (when, and what a close drops) is the game's. Source: RiskMetrics Technical Document, 4th ed. (J.P. Morgan/Reuters, 1996), 4.1.1, Eq. [4.3] and Table 4.1. Checked against a reference in numpy, which has its own tests.
- Source
- RiskMetrics Technical Document, 4th ed. (J.P. Morgan/Reuters, 1996), 4.1.1, Eq. [4.3] and Table 4.1.
- Proof
- reference, invariant, limit.
log_return.trait
namespace lusine.finance;
float log_return(float<_> price, float<_> previous)
{
return log(price / previous);
}
A price's log return from the one before, ln(p / p0).
import os, sys
root = os.getcwd()
while not os.path.isfile(os.path.join(root, 'docsite.json')):
root = os.path.dirname(root)
sys.path.insert(0, os.path.join(root, 'tools'))
import numpy as np
import matplotlib.pyplot as plt
from scipy.special import ndtr
from scipy.stats import norm
from lusine import trait_run
EPS = float(np.finfo(np.float32).eps) # Trait's float is binary32
YEAR = 31557600.0 # lusine's year row, the Julian year in seconds: the host speaks base units
The kernel¶
The law as the library states it, and as this page runs it.
print(trait_run.source('finance/log_return.trait'))
// lusine.finance.log_return: a price's log return from the one before, r = ln(p / p0): the continuously compounded
// return between two samples, dimensionless. Generic over the game's money unit, as crossing_price is: both prices in
// one unit, which divides out. The sampling (when, and what a close drops) is the game's.
// Source: RiskMetrics Technical Document, 4th ed. (J.P. Morgan/Reuters, 1996), 4.1.1, Eq. [4.3] and Table 4.1.
// Checked against a reference in numpy, which has its own tests.
namespace lusine.finance;
float log_return(float<_> price, float<_> previous)
{
return log(price / previous);
}
The program¶
A lattice of samples, each a price and the one before, the kernel bound twice by the one law: to prices in dollars (floats) and in yen (integers). The kernel is generic over its money; the units divide out.
RET = """namespace ret;
lusine.version version = { { number = 1 } };
struct sample {
host float<USD> price; host float<USD> previous; float r;
host integer<JPY> price_yen; host integer<JPY> previous_yen; float r_yen;
};
lattice<sample, 1> samples(spacing = 1 m, boundary = wall);
void main()
{
samples.r = lusine.finance.log_return(samples.price, samples.previous);
samples.r_yen = lusine.finance.log_return(price = samples.price_yen, previous = samples.previous_yen);
}
"""
print(RET)
def returns(price, previous, price_yen=1, previous_yen=1):
"""Each sample's log return, from dollars and from yen."""
n = max(np.size(price), np.size(price_yen))
run = trait_run.Program(['finance/log_return.trait'], RET, {'ret.samples': [n]})
for field, v in (('price', price), ('previous', previous), ('price_yen', price_yen),
('previous_yen', previous_yen)):
run.store('ret.samples', field, np.broadcast_to(np.asarray(v), (n,)))
r, ry = run.steps('ret.main', 1, [('ret.samples', 'r'), ('ret.samples', 'r_yen')])
return r[0], ry[0]
namespace ret;
lusine.version version = { { number = 1 } };
struct sample {
host float<USD> price; host float<USD> previous; float r;
host integer<JPY> price_yen; host integer<JPY> previous_yen; float r_yen;
};
lattice<sample, 1> samples(spacing = 1 m, boundary = wall);
void main()
{
samples.r = lusine.finance.log_return(samples.price, samples.previous);
samples.r_yen = lusine.finance.log_return(price = samples.price_yen, previous = samples.previous_yen);
}
Reference: RiskMetrics' Table 4.1¶
USD/DEM, 29 March to 12 April 1996 (RiskMetrics Technical Document, 4th ed., 1996, Table 4.1): each day's log price change, in percent, is the table's to the 0.001% it prints.
P = np.array([0.67654, 0.67732, 0.67422, 0.67485, 0.67604, 0.67545, 0.67449, 0.67668, 0.67033,
0.66680, 0.66609, 0.66503])
printed = np.array([0.115, -0.459, 0.093, 0.176, -0.087, -0.142, 0.324, -0.943, -0.528, -0.107, -0.159])
r, _ = returns(P[1:], P[:-1])
assert np.max(np.abs(100 * r - printed)) <= 0.0005, 100 * r
print(np.round(100 * r, 4))
[ 0.1152 -0.4587 0.0934 0.1762 -0.0873 -0.1422 0.3242 -0.9428 -0.528 -0.1065 -0.1593]
Invariant: returns add, in any money¶
The days' log returns add to the return over them all (RiskMetrics' Eq. [4.7]) within binary32's rounding, an EPS apiece (each is the log of a ratio near one, rounded to an EPS of it); a rise and the fall back are opposite, to an EPS; and integer prices in yen give the dollars' returns.
r, _ = returns(P[1:], P[:-1])
whole, _ = returns(P[-1], P[0])
assert abs(np.sum(r) - whole[0]) <= (len(r) + 1) * EPS, (np.sum(r), whole)
up, yen = returns([110.0, 100.0], [100.0, 110.0], [11000, 10000], [10000, 11000])
assert abs(up[0] + up[1]) <= 2 * EPS and abs(up[0] - np.log(1.1)) <= 2 * EPS, up
assert np.array_equal(up, yen), (up, yen)
print('ten percent up:', up[0], ' in yen:', yen[0])
ten percent up: 0.09531020373106003 in yen: 0.09531020373106003
Limit: no move¶
A price that has not moved returns exactly nothing.
r, _ = returns([0.67654, 101.25], [0.67654, 101.25])
assert list(r) == [0.0, 0.0]