*! _gvar_unitroot 1.0.1  21aug2026
*! gvar unitroot -- unit-root tests on the domestic, foreign-specific and
*! global variable blocks.
*! Author: Merwan Roudane
*! merwanroudane920@gmail.com
*! https://github.com/merwanroudane
*
* Step -> source map
*   ADF, lag by AIC or SBC                  <- Toolbox adf.m, unitroot_tests.m
*   weighted-symmetric Dickey-Fuller        <- Toolbox ws.m (Park-Fuller)
*   WS read at the ADF-selected lag         <- Toolbox unitroot_tests.m
*   four blocks: levels +/- trend, D, DD    <- Toolbox unitroot_tests.m
*   DdPS asymptotic 5% critical values      <- Toolbox unitroot_tests.m
*   ADF-GLS, KPSS, Phillips-Perron          <- added (standard practice)

program define _gvar_unitroot, rclass
    version 14.0

    syntax [, MAXLag(integer 4)        ///
              IC(string)               ///
              TESTs(string)            ///
              BLOCKs(string)           ///
              DOMestic                 ///
              FOReign                  ///
              noSUMmary                ///
              SAVing(name)             ///
              GRaph                    ///
              GSTat(string)            ///
              GBLock(integer 1)        ///
              NAME(string) ]

    _gvar_require foreign

    * ---- which information criterion picks the ADF lag --------------------
    if ("`ic'" == "") local ic sbc
    local ic = lower("`ic'")
    if ("`ic'" == "aic")      local icsel 2
    else if ("`ic'" == "sbc") local icsel 3
    else {
        di as err "ic() must be {bf:aic} or {bf:sbc}"
        exit 198
    }

    * ---- which tests -------------------------------------------------------
    if ("`tests'" == "") local tests "adf ws"
    local tests = lower("`tests'")
    foreach t of local tests {
        if (!inlist("`t'", "adf", "ws", "gls", "kpss", "pp")) {
            di as err "tests(): unknown test {bf:`t'}"
            di as err "choose from adf, ws, gls, kpss, pp"
            exit 198
        }
    }

    * ---- which blocks ------------------------------------------------------
    if ("`blocks'" == "") local blocks "1 2 3 4"
    foreach b of local blocks {
        if (!inlist(`b', 1, 2, 3, 4) ) {
            di as err "blocks() must list values from 1 to 4"
            exit 198
        }
    }

    * ---- which variable blocks --------------------------------------------
    local kinds ""
    if ("`domestic'" != "") local kinds "1"
    if ("`foreign'"  != "") local kinds "`kinds' 2"
    if ("`kinds'" == "")    local kinds "1 2"

    * =======================================================================
    * Run the battery
    * =======================================================================
    tempname R
    mata: st_matrix("`R'", gvar_urt(`maxlag', `icsel'))
    mata: st_local("cn", invtokens(gvar_getcname()'))
    mata: st_local("N",  strofreal(gvar_getN()))

    local nr = rowsof(`R')

    * =======================================================================
    * Report
    * =======================================================================
    * The report loop was never guarded, so nosummary had nothing to switch
    * off -- and the option was declared as SUMmary, whose local nothing read.
    * There was no way to run this command quietly except -quietly-.
    if ("`summary'" == "nosummary") local kinds ""

    foreach kind of local kinds {
        if ("`kind'" == "1") local kindlab "Domestic variables"
        else                 local kindlab "Foreign-specific variables (x*)"

        foreach b of local blocks {
            if (`b' == 1) local blab "levels, intercept and trend"
            if (`b' == 2) local blab "levels, intercept only"
            if (`b' == 3) local blab "first differences, intercept only"
            if (`b' == 4) local blab "second differences, intercept only"

            foreach t of local tests {
                * Stata requires { to be the last thing on its line and }
                * to stand alone, so these cannot be written as one-liners.
                *
                * col is the statistic; cvc is the column PRINTED as "the
                * critical value" in the last column of the table.  For
                * ADF-GLS and KPSS that printed value does NOT govern the
                * stars -- their cutoffs are constants set in _gvar_urt_table,
                * because GLS detrending and a stationarity null both have
                * their own distributions.  The footer of each table says
                * which number produced its stars.
                if ("`t'" == "adf") {
                    local col  5
                    local cvc  6
                    local tlab "ADF"
                }
                if ("`t'" == "ws") {
                    local col  8
                    local cvc  9
                    local tlab "Weighted symmetric (WS)"
                }
                if ("`t'" == "gls") {
                    local col 10
                    local cvc  6
                    local tlab "ADF-GLS"
                }
                if ("`t'" == "kpss") {
                    local col 11
                    local cvc  6
                    local tlab "KPSS"
                }
                if ("`t'" == "pp") {
                    local col 12
                    local cvc  6
                    local tlab "Phillips-Perron Z(t)"
                }

                tempname TAB
                mata: st_matrix("`TAB'", gvar_urt_tab(st_matrix("`R'"), ///
                                         `kind', `b', `col', `cvc'))
                _gvar_urt_table `TAB' "`__urtvars'" "`cn'" "`t'" "`tlab'" ///
                    "`kindlab' -- `blab'" `b'
            }
        }
    }

    if ("`saving'" != "") {
        matrix `saving' = `R'
        di as text "Full results saved in matrix {bf:`saving'} " ///
                   "(kind unit var block adf cv lag ws cv gls kpss pp)."
    }

    * ---- graph --------------------------------------------------------------
    * One test for one block, each statistic against its own 5% cutoff.
    * For the four t-type tests a rejection is the statistic falling BELOW the
    * cutoff -- both are negative -- so the marked points are the STATIONARY
    * series, which in a levels block is the interesting minority.  KPSS runs
    * the other way: its null is stationarity, its statistic is positive, and
    * a rejection is a LARGE value.  Getting that backwards marks exactly the
    * complement and still draws a plausible-looking picture.
    if ("`graph'" != "") {
        local gs = lower("`gstat'")
        if ("`gs'" == "") local gs adf
        if (!inlist("`gs'", "adf", "ws", "gls", "kpss", "pp")) {
            di as err "gstat() must be {bf:adf}, {bf:ws}, {bf:gls}," ///
                      " {bf:kpss} or {bf:pp}"
            exit 198
        }
        local gblk = `gblock'
        if (`gblk' < 1 | `gblk' > 4) {
            di as err "gblock() must be 1, 2, 3 or 4"
            exit 198
        }
        * the matrix holds BOTH the domestic and the foreign block, so the
        * kind has to be filtered as well or the two get interleaved
        local gk 1
        if ("`domestic'" == "" & "`foreign'" != "") local gk 2

        * Which column holds the statistic, where its cutoff comes from,
        * and which side of the cutoff counts as a rejection.  "row" means the
        * cutoff travels with each row of the results matrix; a number means it
        * is a constant that depends only on whether the block has a trend.
        *   gdir = 1  reject BELOW the cutoff (all the t-type tests)
        *   gdir = 0  reject ABOVE it (KPSS, whose null is stationarity)
        if ("`gs'" == "adf") {
            local gcol 5
            local gcvc "row"
            local gcvr 6
            local gdir 1
            local gnm  "ADF unit-root"
        }
        if ("`gs'" == "ws") {
            local gcol 8
            local gcvc "row"
            local gcvr 9
            local gdir 1
            local gnm  "Weighted-symmetric unit-root"
        }
        if ("`gs'" == "pp") {
            local gcol 12
            local gcvc "row"
            local gcvr 6
            local gdir 1
            local gnm  "Phillips-Perron Z(t)"
        }
        if ("`gs'" == "gls") {
            local gcol 10
            local gdir 1
            local gnm  "ADF-GLS"
            if (`gblk' == 1) local gcvc -2.89
            else             local gcvc -1.95
        }
        if ("`gs'" == "kpss") {
            local gcol 11
            local gdir 0
            local gnm  "KPSS stationarity"
            if (`gblk' == 1) local gcvc 0.146
            else             local gcvc 0.463
        }

        local nq = rowsof(`R')
        tempname G
        matrix `G' = J(`nq', 4, .)
        local nk 0
        forvalues q = 1/`nq' {
            if (`R'[`q', 4] != `gblk') continue
            if (`R'[`q', 1] != `gk')   continue
            local ++nk
            matrix `G'[`nk', 1] = `R'[`q', 2]
            matrix `G'[`nk', 2] = `R'[`q', `gcol']
            if ("`gcvc'" == "row") matrix `G'[`nk', 3] = `R'[`q', `gcvr']
            else                   matrix `G'[`nk', 3] = `gcvc'
            matrix `G'[`nk', 4] = 1
        }
        if (`nk' == 0) {
            di as err "no rows for block `gblk'"
            exit 498
        }
        matrix `G' = `G'[1..`nk', 1...]

        local bnm1 "levels, intercept and trend"
        local bnm2 "levels, intercept only"
        local bnm3 "first differences, intercept only"
        local bnm4 "second differences, intercept only"
        local sub "`bnm`gblk''; marked = rejects at 5%"
        if ("`gs'" == "kpss") {
            local sub "`bnm`gblk''; marked = rejects STATIONARITY at 5%"
        }
        _gvar_dotplot `G' `nk' "`cn'" ///
            "`gnm' statistics" "`sub'" ///
            "statistic" "`name'" `gdir'
    }

    return matrix urt = `R', copy
    return local  ic     "`ic'"
    return scalar maxlag = `maxlag'
end
* ---------------------------------------------------------------------------
* One block per VARIABLE, listing only the units that carry it.
*
* A rectangular units x variables grid cannot avoid blank cells when units own
* different variables: 26 units and 9 variables give 234 cells for 136 real
* results, and no amount of column reshuffling changes that.  The blanks are a
* property of the layout, not of the data.
*
* print_dstats.m is variable-major for this reason -- it writes one block per
* variable with countries as rows.  It still emits NaN rows for countries that
* lack the variable; omitting those rows instead makes a blank impossible.
* Every printed number is a result, and the unit count in each header says how
* many countries carry that series.
*
* Step -> source map
*   block-per-variable layout   <- Toolbox print_dstats.m structure
*   the star rule               <- _gvar_urt_mark, shared with nothing else now
* ---------------------------------------------------------------------------
program define _gvar_urt_table
    version 14.0
    args TAB vlist cn tcode tlab title blk

    local nv : word count `vlist'
    if (`nv' == 0) exit
    local nu : word count `cn'

    * ---- the cutoffs that are NOT stored beside the statistic ---------------
    * Only block 1 carries a trend, and that changes two of the five cutoffs.
    * Getting this wrong is silent: the table still prints, with stars in the
    * wrong places.
    *
    *   ADF, WS   their own 5% value travels with each row (columns 6 and 9)
    *   PP Z(t)   the same asymptotic distribution as the ADF t, so the ADF
    *             value is correct for it and is used
    *   ADF-GLS   Elliott, Rothenberg & Stock (1996): -2.89 with a trend,
    *             -1.95 without.  The ADF value is far too demanding.
    *   KPSS      Kwiatkowski, Phillips, Schmidt & Shin (1992): 0.146 for
    *             trend stationarity, 0.463 for level stationarity.  The null
    *             is STATIONARITY, so rejection is a large POSITIVE value.
    if ("`blk'" == "") local blk 2
    if (`blk' == 1) {
        local gcv -2.89
        local kcv 0.146
        local trlab "with a trend"
    }
    else {
        local gcv -1.95
        local kcv 0.463
        local trlab "without a trend"
    }

    local w 74
    di ""
    di as text "{hline `w'}"
    di as text "  {bf:`tlab'} -- `title'"
    di as text "{hline `w'}"

    local nrej 0
    local ntot 0
    local nfl  0

    forvalues p = 1/`nv' {
        local v : word `p' of `vlist'

        * which units carry this variable, and how many of them reject
        local own ""
        forvalues i = 1/`nu' {
            if (`TAB'[`i', `=2*`nv'+`p''] == 1) local own "`own' `i'"
        }
        local nown : word count `own'
        if (`nown' == 0) continue

        local vrej 0
        foreach i of local own {
            local st = `TAB'[`i', `p']
            local cv = `TAB'[`i', `=`nv'+`p'']
            _gvar_urt_mark "`tcode'" `st' `cv' `kcv' `gcv'
            if ("`r(mark)'" == "*") local ++vrej
        }

        di as text "  {bf:`v'}" _col(14) as text "carried by " ///
           as result `nown' as text " unit(s), " as result `vrej' ///
           as text " reject" _continue
        if (`nown' > 0) {
            di as text " (" as result %4.1f `=100*`vrej'/`nown'' as text "%)"
        }
        else {
            di ""
        }

        * flow the units across the line: four per line keeps it inside 74
        * columns and never leaves a hole, because only owners are printed
        local col 0
        foreach i of local own {
            local u : word `i' of `cn'
            local st = `TAB'[`i', `p']
            local cv = `TAB'[`i', `=`nv'+`p'']
            if (`col' == 0) di as text "    " _continue
            if (`st' >= .) {
                * the unit HAS the variable and the test still produced
                * nothing.  That is a failure and the only thing that earns a
                * dot; there is no other way for a gap to appear here.
                local ++nfl
                * 9 + 6 + 2 = 17 columns per entry, four to a line.  The mark
                * field is TWO wide so a star cannot abut the next unit name.
                di as text %-9s abbrev("`u'", 9) as err %6s "." ///
                   as text %-2s "" _continue
            }
            else {
                local ++ntot
                _gvar_urt_mark "`tcode'" `st' `cv' `kcv' `gcv'
                local mark "`r(mark)'"
                if ("`mark'" == "*") local ++nrej
                di as text %-9s abbrev("`u'", 9) ///
                   as result %6.2f `st' as text %-2s "`mark'" _continue
            }
            local ++col
            if (`col' == 4) {
                di ""
                local col 0
            }
        }
        if (`col' > 0) di ""
        di ""
    }

    di as text "{hline `w'}"
    if (`ntot' > 0) {
        di as text "  Rejections: " as result `nrej' as text " of " ///
                   as result `ntot' as text " (" ///
                   as result %4.1f `=100*`nrej'/`ntot'' as text "%)"
        di as text "  Every printed number is a result.  Only the units that" ///
                   " carry a series"
        di as text "  are listed under it, so a gap in this table is not" ///
                   " possible" _continue
        if (`nfl' > 0) {
            di as text " --"
            di as err "  except the `nfl' dot(s) above, where a unit HAS the" ///
                      " variable and the"
            di as err "  test failed.  Those are a defect, not a" ///
                      " specification fact."
        }
        else {
            di as text ", and there are none."
        }
    }

    * Each footer names the cutoff THIS table actually used, not the pair of
    * values the test has in general: a reader checking a star by hand needs
    * the number that produced it.
    if ("`tcode'" == "adf") {
        di as text "  5% critical value: -3.45 `trlab' (Dees, di Mauro," ///
                   " Pesaran & Smith 2007)."
    }
    if ("`tcode'" == "ws") {
        di as text "  5% critical value: Park & Fuller weighted symmetric," ///
                   " `trlab'."
    }
    if ("`tcode'" == "gls") {
        di as text "  5% critical value " as result "`gcv'" as text ///
                   " (`trlab'), Elliott, Rothenberg & Stock (1996)."
        di as text "  This is {bf:not} the ADF value: GLS detrending changes" ///
                   " the distribution."
    }
    if ("`tcode'" == "pp") {
        di as text "  5% critical value: the ADF one.  The Phillips-Perron" ///
                   " Z(t) shares the"
        di as text "  Dickey-Fuller asymptotic distribution, so it applies" ///
                   " unchanged."
    }
    if ("`tcode'" == "kpss") {
        di as text "  5% critical value " as result "`kcv'" as text ///
                   " (`trlab'), Kwiatkowski, Phillips,"
        di as text "  Schmidt & Shin (1992).  H0 here is {bf:STATIONARITY}," ///
                   " so * means the"
        di as text "  null is rejected and the series looks nonstationary."
    }
    if ("`tcode'" != "kpss") {
        di as text "  * marks rejection of the unit-root null at 5%."
    }
end


* ---------------------------------------------------------------------------
* The one rule that decides whether a unit-root statistic gets a star.
*
* Factored out because it is now needed in two places -- the grid and the
* single-owner block -- and duplicated arithmetic drifts.  The same lesson
* applied to the Ploberger-Kramer path functions, which are checked against
* their summary statistics for exactly this reason.
*
*   KPSS      H0 is STATIONARITY, so a rejection is a LARGE POSITIVE value,
*             against the Kwiatkowski-Phillips-Schmidt-Shin cutoff that
*             depends on whether the block carries a trend.
*   ADF-GLS   Elliott-Rothenberg-Stock cutoff, not the ADF one: GLS
*             detrending changes the distribution.
*   ADF, WS   their own cutoff travels with the row.
*   PP Z(t)   the ADF cutoff, which is correct because the Z(t) shares the
*             Dickey-Fuller asymptotic distribution.
* ---------------------------------------------------------------------------
program define _gvar_urt_mark, rclass
    version 14.0
    args tcode st cv kcv gcv

    local mark " "
    if ("`tcode'" == "kpss") {
        if (`st' < . & `st' > `kcv') local mark "*"
    }
    else if ("`tcode'" == "gls") {
        if (`st' < . & `st' < `gcv') local mark "*"
    }
    else {
        if (`st' < . & `cv' < . & `st' < `cv') local mark "*"
    }
    return local mark "`mark'"
end