Skip to content

Commit 5003faa

Browse files
Garch 1 1 (#47)
* Able to estimate GARCH(1,1) * more opt_methods * remove unecessary method * update GARCH
1 parent 58349c3 commit 5003faa

19 files changed

+2359
-96
lines changed

Project.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ SpecialFunctions = "~0.8"
1616
julia = "~1"
1717

1818
[extras]
19+
DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab"
1920
HypothesisTests = "09f84164-cd44-5f33-b23f-e6b0d136a0d5"
2021
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
2122
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
2223

2324
[targets]
24-
test = ["Test", "Random", "HypothesisTests"]
25+
test = ["DelimitedFiles", "Test", "Random", "HypothesisTests"]

docs/src/manual.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,24 @@
11
# Manual
22

3+
## Model Specification
4+
5+
## Recursion
6+
37
## Links
48

5-
Links are reparametrizations utilized in the estimation. For example, if we want to estimate a parameter ``f`` which is by definition strictly positive, then an obvious way to estimate ``f`` via numerical optimization is to model ``\tilde{f} = \ln{f}``. We refer to this procedure as **linking**. After obtaining the optimal value of ``\tilde{f}``, we can then **unlink** it to obtain ``f`` by computing ``f = e^{\tilde{f}}``.
9+
Links are reparametrizations utilized to ensure certain parameter is within its original domain, i.e. in a distribution one would like to ensure that the time varying parameter ``f \in \mathbb{R}^+``. The way to do this is to model ``\tilde{f} = \ln{f}``. More generally one can stablish that ``\tilde{f} = h(f)``. We refer to this procedure as **linking**. When the parameter is linked the GAS recursion happens in the domain of ``\tilde{f}`` and then one can recover the orginal parameter by ``f = \left(h\right)^-1(\tilde f)``. We refer to this procedure as **unlinking**. The new GAS recursion becomes.
10+
11+
```math
12+
\begin{equation*}\left\{\begin{array}{ccl}
13+
f_{t} &=& h^{-1}(\tilde f_t), \\
14+
\tilde f_{t+1} &=& \omega + \sum_{i=1}^p A_{i}\tilde s_{t-i+1} + \sum_{j=1}^q B_{j}\tilde f_{t-j+1}
15+
\end{array}
16+
\right.
17+
\end{equation*}
18+
```
19+
20+
Notice that the change in parametrization changes the dynamics of the model. The GAS(1,1) for a Normal distribution with inverse scaling ``d = 1`` is equivalent to the GARCH(1, 1) model, but only on the original parameter, if you work with a different parametrization the model is no longer equivalent.
21+
622

723
### Types of links
824

@@ -11,6 +27,7 @@ The abstract type `Link` subsumes any type of link that can be expressed.
1127
```@docs
1228
ScoreDrivenModels.IdentityLink
1329
ScoreDrivenModels.LogLink
30+
ScoreDrivenModels.LogitLink
1431
```
1532

1633
### Link functions

examples/garch.jl

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
using ScoreDrivenModels, Statistics, DelimitedFiles
2+
3+
const SDM = ScoreDrivenModels
4+
5+
# The GARCH(1, 1) is equivaent to a GAS(1, 1) Normal model with inverse scaling. The models are only equivalent under
6+
# no reparametrizations, here this means that the you should fit the model overwriting the default link!, unlink! and
7+
# jacobian_link! methods for the Normal distribution to use only the IdentityLink.
8+
9+
# Overwrite link interface to work with Identity links.
10+
function SDM.link!(param_tilde::Matrix{T}, ::Type{Normal}, param::Matrix{T}, t::Int) where T
11+
param_tilde[t, 1] = link(IdentityLink, param[t, 1])
12+
param_tilde[t, 2] = link(IdentityLink, param[t, 2])
13+
return
14+
end
15+
function SDM.unlink!(param::Matrix{T}, ::Type{Normal}, param_tilde::Matrix{T}, t::Int) where T
16+
param[t, 1] = unlink(IdentityLink, param_tilde[t, 1])
17+
param[t, 2] = unlink(IdentityLink, param_tilde[t, 2])
18+
return
19+
end
20+
function SDM.jacobian_link!(aux::AuxiliaryLinAlg{T}, ::Type{Normal}, param::Matrix{T}, t::Int) where T
21+
aux.jac[1] = jacobian_link(IdentityLink, param[t, 1])
22+
aux.jac[2] = jacobian_link(IdentityLink, param[t, 2])
23+
return
24+
end
25+
26+
# Data from [Bollerslev and Ghysels (JBES 1996)](https://doi.org/10.2307/1392425). Took from ARCHModels.jl
27+
y = readdlm("./test/data/BG96.csv")[:]
28+
29+
# Evaluate some approximate initial params
30+
initial_params = [mean(y) var(y)]
31+
32+
# There are 2 possibilities to estimate the model.
33+
# The first one is to understand that in the GARCH estimation process usually one
34+
# defines that the parameters ω, α_0, α_1 and β_1 are constrained. The problem is
35+
# that the equivalent GAS model relies that
36+
# ω_1 = ω
37+
# ω_2 = α_0
38+
# A_1 = α_1
39+
# B_1 - A_1 = β_1
40+
# Here there is no easy way of adding a constraint B_1 - A_1 >= 0. An alternative is to define
41+
# bounds assuring the upper bound of A_1 is smaller than the lower bound for B_1.
42+
43+
# Define upper bounds and lower bounds for parameters ω_1, ω_2, A_1, B_1
44+
ub = [1.0; 1.0; 0.5; 1.0]
45+
lb = [-1.0; 0.0; 0.0; 0.5]
46+
47+
# Define a GAS model where only \sigma^2 is varying over time
48+
gas = GAS(1, 1, Normal, 1.0, time_varying_params = [2])
49+
50+
# Give an initial_point in the interior of the bounds.
51+
initial_point = [0.0; 0.5; 0.25; 0.75]
52+
53+
# Estimate the model
54+
res = estimate!(gas, y;
55+
initial_params = initial_params,
56+
opt_method = IPNewton(gas, [initial_point]; ub = ub, lb = lb))
57+
58+
59+
# Another way to estimate the model is to rely on luck and give many random initial_points
60+
# for a non constrained optimization
61+
62+
gas = GAS(1, 1, Normal, 1.0, time_varying_params = [2])
63+
res = estimate!(gas, y;
64+
initial_params = initial_params,
65+
opt_method = NelderMead(gas, 100))

src/MLE.jl

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,24 @@ struct EstimationResults{T <: AbstractFloat}
77
aic::T
88
bic::T
99
llk::T
10-
minimizer::Vector{T}
10+
coefs::Vector{T}
1111
numerical_hessian::Matrix{T}
1212
end
1313

14-
function log_lik(psitilde::Vector{T}, y::Vector{T}, sdm::SDM{D, T},
15-
initial_params::Vector{Vector{T}}, unknowns::Unknowns_SDM, n::Int) where {D, T}
16-
return error("log_lik not defined for a model of type ", typeof(sdm))
14+
mutable struct AuxEstimation{T <: AbstractFloat}
15+
psi::Vector{Vector{T}}
16+
numerical_hessian::Vector{Matrix{T}}
17+
loglikelihood::Vector{T}
18+
opt_result::Vector{Optim.OptimizationResults}
19+
20+
function AuxEstimation{T}() where T
21+
return new(
22+
Vector{Vector{T}}(undef, 0), #psi
23+
Vector{Matrix{T}}(undef, 0),
24+
Vector{T}(undef, 0), # loglikelihood
25+
Vector{Optim.OptimizationResults}(undef, 0) # opt_result
26+
)
27+
end
1728
end
1829

1930
function AIC(n_unknowns::Int, log_lik::T) where T
@@ -24,13 +35,23 @@ function BIC(n::Int, n_unknowns::Int, log_lik::T) where T
2435
return T(log(n) * n_unknowns - 2 * log_lik)
2536
end
2637

38+
function update_aux_estimation!(aux_est::AuxEstimation{T}, func::Optim.TwiceDifferentiable,
39+
opt_result::Optim.OptimizationResults) where T
40+
41+
push!(aux_est.loglikelihood, -opt_result.minimum)
42+
push!(aux_est.psi, opt_result.minimizer)
43+
push!(aux_est.numerical_hessian, Optim.hessian!(func, opt_result.minimizer))
44+
push!(aux_est.opt_result, opt_result)
45+
return
46+
end
47+
2748
function estimate!(sdm::SDM{D, T}, y::Vector{T};
2849
initial_params::Matrix{T} = DEFAULT_INITIAL_PARAM,
2950
opt_method::AbstractOptimizationMethod = LBFGS(sdm, DEFAULT_NUM_SEEDS),
3051
verbose::Int = 0) where {D, T}
3152

3253
# Number of seed and number of params to estimate
33-
nseeds = length(opt_method.seeds)
54+
n_seeds = length(opt_method.seeds)
3455
n = length(y)
3556

3657
unknowns = find_unknowns(sdm)
@@ -40,49 +61,39 @@ function estimate!(sdm::SDM{D, T}, y::Vector{T};
4061
check_model_estimated(n_unknowns) && return sdm
4162

4263
# optimize for each seed
43-
psi = Vector{Vector{T}}(undef, 0)
44-
numerical_hessian = Vector{Matrix{T}}(undef, 0)
45-
loglikelihood = Vector{T}(undef, 0)
46-
optseeds = Vector{Optim.OptimizationResults}(undef, 0)
64+
aux_est = AuxEstimation{T}()
4765

48-
for i = 1:nseeds
66+
for i = 1:n_seeds
4967
try
5068
func = TwiceDifferentiable(psi_tilde -> log_lik(psi_tilde, y, sdm, initial_params, unknowns, n), opt_method.seeds[i])
51-
optseed = optimize(func, opt_method.seeds[i],
52-
opt_method.method, Optim.Options(f_tol = opt_method.f_tol,
53-
g_tol = opt_method.g_tol,
54-
iterations = opt_method.iterations,
55-
show_trace = (verbose == 2 ? true : false) ))
56-
push!(loglikelihood, -optseed.minimum)
57-
push!(psi, optseed.minimizer)
58-
push!(numerical_hessian, Optim.hessian!(func, optseed.minimizer))
59-
push!(optseeds, optseed)
60-
println("seed $i of $nseeds - $(-optseed.minimum)")
69+
opt_result = optimize(func, opt_method, verbose, i)
70+
update_aux_estimation!(aux_est, func, opt_result)
71+
println("seed $i of $n_seeds - $(-opt_result.minimum)")
6172
catch err
6273
println(err)
6374
println("seed $i diverged")
6475
end
6576
end
6677

67-
if isempty(loglikelihood)
78+
if isempty(aux_est.loglikelihood)
6879
println("No seed converged.")
6980
return
7081
end
7182

72-
best_llk, best_seed = findmax(loglikelihood)
73-
num_hessian = numerical_hessian[best_seed]
74-
best_psi = psi[best_seed]
83+
best_llk, best_seed = findmax(aux_est.loglikelihood)
84+
num_hessian = aux_est.numerical_hessian[best_seed]
85+
coefs = aux_est.psi[best_seed]
7586
aic = AIC(n_unknowns, best_llk)
7687
bic = BIC(n, n_unknowns, best_llk)
7788

7889
if verbose >= 1
7990
println("\nBest seed optimization result:")
80-
println(optseeds[best_seed])
91+
println(aux_est.opt_result[best_seed])
8192
end
8293

8394
# return the estimated
84-
fill_psitilde!(sdm, best_psi, unknowns)
95+
fill_psitilde!(sdm, coefs, unknowns)
8596

8697
println("Finished!")
87-
return EstimationResults{T}(aic, bic, best_llk, best_psi, num_hessian)
98+
return EstimationResults{T}(aic, bic, best_llk, coefs, num_hessian)
8899
end

src/ScoreDrivenModels.jl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@ include("utils.jl")
1515
include("link_functions.jl")
1616
include("score.jl")
1717
include("MLE.jl")
18-
include("opt_methods.jl")
18+
19+
# Optimization methods
20+
include("opt_methods/common_methods.jl")
21+
include("opt_methods/LBFGS.jl")
22+
include("opt_methods/IPNewton.jl")
23+
include("opt_methods/NelderMead.jl")
1924

2025
# GAS
2126
include("gas/gas.jl")

src/abstracts.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
abstract type ScoreDrivenModel{D, T} end
22

3-
abstract type Unknowns_SDM end
3+
abstract type UnknownsSDM end
44

55
"""
66
AbstractOptimizationMethod

src/distributions/common_interface.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ const DISTS = [
88
Weibull
99
]
1010

11+
export Normal, Beta, Poisson, LogNormal, Gamma, Weibull
12+
1113
function score!(score_til::Matrix{T}, y::T, ::Type{<:Distribution}, param::Matrix{T}, t::Int) where T
1214
return error("score! not implemented for $D distribution")
1315
end

src/gas/gas.jl

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,13 @@ find_unknowns
6464
dim_unknowns
6565
length
6666
"""
67-
mutable struct Unknowns_GAS <: Unknowns_SDM
67+
mutable struct UnknownsGAS <: UnknownsSDM
6868
ω::Vector{Int}
6969
A::Dict{Int, Vector{Int}}
7070
B::Dict{Int, Vector{Int}}
7171
end
7272

73-
function fill_psitilde!(gas::GAS, psitilde::Vector{T}, unknowns::Unknowns_GAS) where T
73+
function fill_psitilde!(gas::GAS, psitilde::Vector{T}, unknowns::UnknownsGAS) where T
7474
offset = 0
7575
# fill ω
7676
for i in unknowns.ω
@@ -106,14 +106,14 @@ function find_unknowns(gas::GAS)
106106
for (k, v) in gas.B
107107
unknowns_B[k] = find_unknowns(v)
108108
end
109-
return Unknowns_GAS(unknowns_ω, unknowns_A, unknowns_B)
109+
return UnknownsGAS(unknowns_ω, unknowns_A, unknowns_B)
110110
end
111111

112112
function dim_unknowns(gas::GAS)
113113
return length(find_unknowns(gas))
114114
end
115115

116-
function length(unknowns::Unknowns_GAS)
116+
function length(unknowns::UnknownsGAS)
117117
len = length(values(unknowns.ω))
118118
for (k, v) in unknowns.A
119119
len += length(v)
@@ -125,7 +125,7 @@ function length(unknowns::Unknowns_GAS)
125125
end
126126

127127
function log_lik(psitilde::Vector{T}, y::Vector{T}, gas::GAS{D, T},
128-
initial_params::Matrix{T}, unknowns::Unknowns_GAS, n::Int) where {D, T}
128+
initial_params::Matrix{T}, unknowns::UnknownsGAS, n::Int) where {D, T}
129129

130130
# Use the unkowns vectors to fill the right positions
131131
fill_psitilde!(gas, psitilde, unknowns)

src/link_functions.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
export IdentityLink, LogLink, LogitLink
2+
export link, unlink, jacobian_link
3+
14
abstract type Link end
25

36
"""

src/opt_methods.jl

Lines changed: 0 additions & 49 deletions
This file was deleted.

0 commit comments

Comments
 (0)