*! _gvar_wetest 1.0.1  21aug2026
*! gvar wetest -- test the weak exogeneity of the foreign-specific and global
*! variables of each country model.
*! Author: Merwan Roudane
*! merwanroudane920@gmail.com
*! https://github.com/merwanroudane
*
* Step -> source map
*   marginal model for Dx*_i on a constant, the ECM terms of unit i and
*   lagged differences of the domestic and foreign variables, F-testing
*   H0: all ECM coefficients are zero   <- Toolbox test_weakexogeneity.m
*   lag orders of the marginal model    <- Toolbox select_lags_we.m

program define _gvar_wetest, rclass
    version 14.0

    * ls() and ln() are the lag orders of the marginal model.  Default 1 and 1.
    *
    * The Toolbox has THREE possible sources for these, selected by lagselect_we:
    *   lagselect_we = 0     inherit the estimation orders (p_i, q_i)
    *                        -- gvar.m:1767-1771
    *   lagselect_we = aic   choose per unit by AIC, from a maximum read off MAIN
    *   lagselect_we = sbc   the same by SBC
    *                        -- gvar.m:1735-1748, aic_sbc_we
    *
    * The shipped full demo uses AIC with a maximum of (2,2) (MAIN row 55), and
    * its exogeneity_test sheet shows the result: 24 of 26 units land on (1,1),
    * arg on (2,1) and chl on (1,2).  Note chl's q* = 2 exceeds its estimation
    * q = 1, which inheritance cannot produce -- that is the proof the demo
    * selected rather than inherited.
    *
    * So (1,1) reproduces 24 of the 26 units and is the right default until the
    * AIC selection is implemented.  A brief experiment defaulting these to the
    * per-unit (p_i, q_i) made things worse -- matching 7 units instead of 24 --
    * because inheritance is the branch the demo did NOT take.
    *
    * Passing ls(-1) or ln(-1) still requests inheritance, which is faithful to
    * the lagselect_we = 0 branch and is a legitimate thing to want.
    *
    * TODO, for exact reproduction: select ls and ln per unit by AIC over
    * 1..maxls, 1..maxln, as select_lags_we.m does.
    *
    * weforeign() names the marginal model's FOREIGN REGRESSOR block, which the
    * Toolbox specifies separately from the unit's weakly exogenous block:
    * fvflag_we / gvflag_we, read at gvar.m:1680-1714, defaulting to the
    * estimation spec (fvflag_we_tmp = fvflag, gvar.m:1632) but then offered to
    * the user for editing at a pause.  The default here is therefore empty --
    * the estimation block alone, matching gvar.m's default.
    *
    * The names are DOMESTIC variable names; their foreign counterparts are what
    * get added.  A global variable name adds the series itself, and is ignored
    * for any unit holding it endogenous.  The left-hand side is untouched, so
    * the shape of the table below does not change.
    *
    * Leave it alone to reproduce the demo.  gvar.m's note at that pause offers
    * "include the foreign variable, eps, in all country models" for DdPS(2007),
    * but that is NOT what the published exogeneity_test sheet was run with: the
    * sheet's degrees of freedom are reproduced by the default block for 24 of
    * the 26 units and by weforeign(ep) for none of them.  bra alone settles it,
    * at a published degfr of 118 against 117 with the extra column.
    syntax [, LS(integer 1) LN(integer 1) LEVel(real 95) noSUMmary ///
              SAVing(name) GRaph NAME(string)                      ///
              SELect(string) MAXLS(integer 2) MAXLN(integer 2)      ///
              WEForeign(string) ]

    * select(aic|sbc) chooses ls and ln per unit, as the demo does.  Without it
    * the given ls/ln apply to every unit.
    local selno 0
    if ("`select'" != "") {
        local select = lower("`select'")
        if      ("`select'" == "aic") local selno 2
        else if ("`select'" == "sbc") local selno 3
        else {
            di as err "select() must be {bf:aic} or {bf:sbc}"
            exit 198
        }
        if (`maxls' < 1 | `maxln' < 1) {
            di as err "maxls() and maxln() must be at least 1"
            exit 198
        }
    }

    _gvar_require estimate

    mata: st_local("N",  strofreal(gvar_getN()))
    mata: st_local("cn", invtokens(gvar_getcname()'))
    mata: st_local("vn", invtokens(gvar_getvname()'))
    mata: st_local("gn", invtokens(gvar_getgvname()'))

    * Reject an unknown name rather than silently fitting a smaller model: a
    * typo in weforeign() would otherwise change every F in the table with no
    * hint that it had, which is the failure mode this whole option exists to
    * fix.  Duplicates are dropped in gvar_wesi(), not here.
    local weadd ""
    foreach v of local weforeign {
        local pv : list posof "`v'" in vn
        local pg : list posof "`v'" in gn
        if (`pv' == 0 & `pg' == 0) {
            di as err "weforeign(): {bf:`v'} is not a variable of this model"
            di as err "  domestic names: `vn'"
            if ("`gn'" != "") di as err "  global names:   `gn'"
            exit 111
        }
        local weadd "`weadd' `v'"
    }
    local weadd = trim("`weadd'")

    tempname R
    * If select() was given, choose (ls, ln) per unit first and pass them down.
    tempname SEL
    if (`selno' > 0) {
        mata: st_matrix("`SEL'", gvar_welagsel(`maxls', `maxln', `selno', ///
                                               tokens("`weadd'")'))
    }
    else {
        matrix `SEL' = J(1, 1, 0)
    }
    mata: st_matrix("`R'", gvar_wetest_all(`ls', `ln', `=`level'/100', ///
                                           st_matrix("`SEL'"),        ///
                                           tokens("`weadd'")'))
    local nr = rowsof(`R')
    if (`nr' == 0) {
        di as err "no unit has both weakly exogenous variables and a positive rank"
        exit 459
    }

    * Column order follows the model's variable order (domestic names, then
    * global names), not the order in which units happen to introduce them,
    * so the table reads the same way as every other table in the package.
    local seen ""
    forvalues i = 1/`N' {
        mata: st_local("sl", invtokens(gvar_getslist(`i')'))
        foreach s of local sl {
            local p : list posof "`s'" in seen
            if (`p' == 0) local seen "`seen' `s'"
        }
    }
    local wl ""
    foreach v of local vn {
        local p : list posof "`v'" in seen
        if (`p' > 0) local wl "`wl' `v'"
    }
    foreach v of local seen {
        local p : list posof "`v'" in wl
        if (`p' == 0) local wl "`wl' `v'"
    }
    local wl = trim("`wl'")
    local nw : word count `wl'

    * The counters are accumulated whether or not the table is printed:
    * -return- must always have something to return.
    local nrej 0
    local ntot 0
    local nmiss 0

    if ("`summary'" != "nosummary") {
        _gvar_title "Weak exogeneity test: F statistics"
        di as text "  H0: the error-correction terms of the country model do not"
        di as text "  enter the marginal model for the foreign variable."
        * Report what was actually used.  Printing "domestic 1, foreign 1"
        * unconditionally would now be a lie for the default path, where the
        * orders differ from unit to unit.
        if (`selno' > 0) {
            di as text "  Marginal model lags: chosen per unit by " ///
               as result upper("`select'") as text " over 1.." ///
               as result `maxls' as text " x 1.." as result `maxln' as text "."
            di as text "  This is what the Toolbox demo does (MAIN row 55: aic,"
            di as text "  maximum 2 and 2), and it is why 19 of its 26 units end"
            di as text "  up with orders differing from their estimation orders."
            mata: st_local("selshow", gvar_welagshow(st_matrix("`SEL'")))
            di as text "  Selected: " as result "`selshow'"
        }
        else if (`ls' < 0 & `ln' < 0) {
            di as text "  Marginal model lags: each unit's own VARX* orders"
            di as text "  (p_i, q_i), as the Toolbox uses by default."
        }
        else if (`ls' < 0) {
            di as text "  Marginal model lags: domestic p_i per unit" ///
                       as text ", foreign " as result `ln' as text "."
        }
        else if (`ln' < 0) {
            di as text "  Marginal model lags: domestic " as result `ls' ///
                       as text ", foreign q_i per unit."
        }
        else {
            di as text "  Marginal model lags: domestic " as result `ls' ///
                       as text ", foreign " as result `ln' ///
                       as text " for every unit (overriding p_i, q_i)."
        }
        * Say which marginal model was fitted.  Silence here is what let the
        * wrong regressor block go unnoticed: the table looked complete either
        * way, because the block affects the F values and nothing visible.
        if ("`weadd'" == "") {
            di as text "  Marginal model foreign block: each unit's own weakly"
            di as text "  exogenous variables -- the Toolbox default, and what"
            di as text "  the published demo used."
        }
        else {
            di as text "  Marginal model foreign block: each unit's own weakly"
            di as text "  exogenous variables plus " as result "`weadd'" ///
               as text "* where not already"
            di as text "  present -- the Toolbox's fvflag_we spec (gvar.m:1680)."
            di as text "  {bf:Note} this is NOT the published demo's block; it"
            di as text "  changes the degrees of freedom.  See {help gvar_methods}."
        }
        di ""
        local w = 14 + 9 * `nw'
        if (`w' > 120) local w 120
        di as text "{hline `w'}"
        di as text %-13s "  Unit" _continue
        foreach v of local wl {
            di as text %9s abbrev("`v'*", 8) _continue
        }
        di as text %8s "F cv95"
        di as text "{hline `w'}"
    }

    forvalues i = 1/`N' {
        local u : word `i' of `cn'
        local any 0
        forvalues q = 1/`nr' {
            if (`R'[`q', 1] == `i') local any 1
        }
        if (`any' == 0) continue

        mata: st_local("sl", invtokens(gvar_getslist(`i')'))
        if ("`summary'" != "nosummary") {
            di as text "  " %-11s abbrev("`u'", 11) _continue
        }
        foreach v of local wl {
            local pos : list posof "`v'" in sl
            if (`pos' == 0) {
                * A DASH means the variable is not in this unit's model -- a fact
                * about the specification.  A DOT is reserved for a test that
                * could not be computed, below.  One symbol for both made the
                * table unreadable: 25 of 26 rows are blank under ep* because
                * only the usa has a foreign exchange rate, and that looked
                * identical to 25 failures.
                if ("`summary'" != "nosummary") di as text %9s "-" _continue
            }
            else {
                local fv .
                local cv .
                forvalues q = 1/`nr' {
                    if (`R'[`q',1] == `i' & `R'[`q',2] == `pos') {
                        local fv = `R'[`q', 6]
                        local cv = `R'[`q', 5]
                        continue, break
                    }
                }
                if (`fv' >= .) {
                    * A DOT here is a genuine failure: the variable IS in the
                    * model but the auxiliary regression did not produce an F.
                    local ++nmiss
                    if ("`summary'" != "nosummary") di as text %9s "." _continue
                }
                else {
                    local ++ntot
                    local mk " "
                    if (`cv' < . & `fv' > `cv') {
                        local mk "*"
                        local ++nrej
                    }
                    if ("`summary'" != "nosummary") {
                        di as result %8.2f `fv' as text "`mk'" _continue
                    }
                }
            }
        }
        local cvi .
        forvalues q = 1/`nr' {
            if (`R'[`q',1] == `i') {
                local cvi = `R'[`q', 5]
                continue, break
            }
        }
        if ("`summary'" != "nosummary") di as result %8.2f `cvi'
    }

    if ("`summary'" != "nosummary") {
        di as text "{hline `w'}"
        di as text "  * marks rejection of weak exogeneity at " ///
                   as result `=100-`level'' as text "%: " ///
                   as result `nrej' as text " of " as result `ntot' ///
                   as text " (" as result %4.1f `=100*`nrej'/max(`ntot',1)' ///
                   as text "%)."
        * A dot is printed in TWO cases -- the variable is not in that unit's
        * weakly exogenous set (a specification fact), or it is and the statistic
        * came back missing (a failure, counted in nmiss).  This sentence used to
        * assert the first meaning unconditionally, which flatly contradicted the
        * "`nmiss' tests could not be computed" warning printed a few lines below
        * whenever nmiss > 0.  Two meanings for one symbol is bad enough; denying
        * it in the legend is worse, because the legend is what a reader believes.
        di as text "  {bf:-} means the variable is not in that unit's model."
        di as text "  Only the usa has a foreign exchange rate, for instance, so"
        di as text "  {bf:ep*} is a dash for the other 25; and the usa's foreign"
        di as text "  block is y*, Dp*, ep* alone, so its other columns are"
        di as text "  dashes.  Both follow the specification, not the data."
        if (`nmiss' == 0) {
            di as text "  {bf:.} would mark a test that could not be computed."
            di as text "  There are none: all " as result `ntot' ///
               as text " applicable tests were computed."
        }
        else {
            di as text "  {err:.} marks a test that could not be computed --" ///
               as text " {err:`nmiss'} of them; see below."
        }
        di as text "  The last column is the " ///
                   as result `=100-`level'' as text "% critical"
        di as text "  value, which varies with the unit's cointegrating rank."
        di as text "  Rejection means the variable is not weakly exogenous for the"
        di as text "  long-run parameters of that unit; see {help gvar_methods}."
        di as text "  Dees, di Mauro, Pesaran & Smith (2007) report rejection rates"
        di as text "  of roughly 5-10% for a correctly specified GVAR."
        if (`nmiss' > 0) {
            di as text "  {err:`nmiss'} tests could not be computed"
            di as text "  even though the variable is in the model; this is a"
            di as text "  failure, not a specification fact."
        }
        di ""
    }

    * ---- graph --------------------------------------------------------------
    * The whole point of the table is the share of rejections, and a share is
    * easier to see than to count: this puts every F beside its own 5% critical
    * value, which differs by unit because the restriction count does.
    if ("`graph'" != "") {
        tempname G
        matrix `G' = J(`nr', 4, .)
        forvalues q = 1/`nr' {
            matrix `G'[`q', 1] = `R'[`q', 1]
            matrix `G'[`q', 2] = `R'[`q', 6]
            matrix `G'[`q', 3] = `R'[`q', 5]
            matrix `G'[`q', 4] = 1
        }
        local sub "F test that the ECM terms are absent from the marginal model; cutoff at `=100-`level''%"
        _gvar_dotplot `G' `nr' "`cn'" ///
            "Weak exogeneity of the foreign variables" "`sub'" ///
            "F statistic" "`name'" 0
    }

    if ("`saving'" != "") {
        matrix `saving' = `R'
    }
    return matrix wetest = `R', copy
    return scalar nrej  = `nrej'
    return scalar ntot  = `ntot'
    return scalar nmiss = `nmiss'
end