*! _gvar_report 1.0.1  21aug2026
*! gvar report -- one specification audit of the fitted model.
*! Author: Merwan Roudane
*! merwanroudane920@gmail.com
*! https://github.com/merwanroudane
*
* Runs the checks a referee would ask for, in the order they matter, and
* summarises each to a line or two rather than printing every table.  It is a
* triage tool: it tells you which of the detailed subcommands to go and read.
*
* Nothing here is new arithmetic; every number comes from the subcommand
* named beside it.

program define _gvar_report, rclass
    version 14.0

    * report is nothing but display, so it never declared nosummary -- which
    * made it the one subcommand that ERRORED on an option every other one
    * accepts.  The body runs quietly, so the audit still populates r() for a
    * caller that wants the numbers without the text.
    *
    * report is also the ONLY subcommand that silences itself with quietly;
    * every other one guards its displays with an if.  That difference is not
    * cosmetic: quietly does NOT suppress -display as error-, because Stata
    * treats error output as something a user must always see.  The audit used
    * red for emphasis inside its table, and under nosummary those words
    * escaped on their own -- "not solved" printed while the -as text- around
    * it vanished.  Emphasis in the body is therefore {err:...} markup inside
    * an -as text- display, which quietly does suppress.  -as err- is reserved
    * here for a message that precedes an exit.
    syntax [, PSC(integer 4) STEP(integer 24) noSUMmary ///
              GRaph NAME(string) PANels(string) ]

    if ("`summary'" == "nosummary") {
        quietly _gvar_report_body `psc' `step'
    }
    else {
        _gvar_report_body `psc' `step'
    }
    return add

    * The dashboard runs AFTER the audit and outside the quietly, so that
    * -nosummary graph- still says where the page went.  Its panels come from
    * the same subcommands the audit just called; nothing is recomputed here
    * that the text above did not already report.
    if ("`graph'" != "") {
        _gvar_report_dash "`name'" `psc' `step' "`panels'"
        return add
    }
end

program define _gvar_report_body, rclass
    version 14.0
    args psc step

    _gvar_require estimate

    mata: st_local("N",  strofreal(gvar_getN()))
    mata: st_local("K",  strofreal(gvar_getK()))
    mata: st_local("T",  strofreal(gvar_getT()))
    mata: st_local("sv", strofreal(gvar_issolved()))

    * When a dominant unit is present, K counts its variables but they belong to
    * no country model, so the diagnostics below cover fewer equations than K.
    * Reporting "136 endogenous variables" above a table of 133 equations reads
    * as three lost results.  Say the split instead.
    capture mata: st_local("hasdu", strofreal(gvar_hasdu()))
    local ndu 0
    if (_rc == 0 & "`hasdu'" == "1") {
        mata: st_local("ndu", strofreal(rows(gvar_getduylist())))
    }

    _gvar_title "GVAR specification audit"
    di as text "  " as result `N' as text " units, " as result `K' ///
       as text " endogenous variables, " as result `T' as text " periods."
    if (`ndu' > 0) {
        di as text "  Of those " as result `K' as text ", " as result `ndu' ///
           as text " belong to the dominant unit and " ///
           as result `=`K' - `ndu'' as text " to the " as result `N' ///
           as text " country models."
        di as text "  The dominant unit has no VECMX*, so the residual and"
        di as text "  cointegration checks below cover " ///
           as result `=`K' - `ndu'' as text " equations, not " as result `K' ///
           as text "."
        di as text "  Its own diagnostics come from {bf:gvar dominant}; the"
        di as text "  Toolbox reports them separately too (DUmodel_augres_sc)."
    }
    di ""

    * ---- 1. the solved system ----------------------------------------------
    di as text "{hline 74}"
    di as text "  {bf:1. Does the system solve?}" _col(58) "{it:gvar solve}"
    di as text "{hline 74}"
    if ("`sv'" != "1") {
        di as text "  {err:not solved}.  Run {bf:gvar solve}."
    }
    else {
        qui gvar solve, nosummary
        local mm = r(maxmod)
        local nexp = r(nexpl)
        local nu = r(nunit)
        local ex = r(expected)
        di as text "  largest eigenvalue modulus  " as result %10.6f `mm'
        if (`nexp' > 0) {
            di as text "  {err:`nexp'} explosive root(s): every" ///
               " impulse response is meaningless until this is fixed."
        }
        else {
            di as text "  no explosive roots."
        }
        di as text "  unit roots found " as result `nu' as text ", implied by" ///
           " the ranks " as result `ex' _continue
        if (`nu' == `ex') di as text "   {bf:agree}"
        else              di as text "   {err:{bf:disagree}}"
        if (`nu' != `ex') {
            di as text "  A mismatch means the ranks and the dynamics" ///
                       " disagree; the usual"
            di as text "  cause is an overstated rank.  See {bf:gvar coint}" ///
                       " and {bf:gvar pp}."
        }
    }
    di ""

    * ---- 2. weak exogeneity -------------------------------------------------
    di as text "{hline 74}"
    di as text "  {bf:2. Are the foreign variables weakly exogenous?}" ///
               _col(56) "{it:gvar wetest}"
    di as text "{hline 74}"
    qui gvar wetest, nosummary
    local nrej = r(nrej)
    local ntot = r(ntot)
    local nmis = r(nmiss)
    local pct  = 100 * `nrej' / max(`ntot', 1)
    di as text "  rejected at 5%: " as result `nrej' as text " of " ///
       as result `ntot' as text "  (" as result %4.1f `pct' as text "%)"
    di as text "  Dees, di Mauro, Pesaran & Smith report 5-10% for a" ///
               " correctly specified GVAR."
    if (`pct' > 15) {
        di as text "  {err:Above that range}: the assumption" ///
           " the whole estimation rests on"
        di as text "  is in doubt.  Read {bf:gvar wetest} in full."
    }
    if (`nmis' > 0) {
        di as text "  {err:`nmis'} test(s) could not be computed."
    }
    di ""

    * ---- 3. cross-section dependence ---------------------------------------
    di as text "{hline 74}"
    di as text "  {bf:3. Did the foreign variables absorb the dependence?}" ///
               _col(56) "{it:gvar avgcorr}"
    di as text "{hline 74}"
    qui gvar avgcorr, nosummary
    tempname AC
    matrix `AC' = r(avgcorr)
    mata: gvar_reportcorr(st_matrix("`AC'"))
    local ml = r(mlev)
    local mr = r(mres)
    di as text "  mean |correlation| in the levels    " as result %8.4f `ml'
    di as text "  mean |correlation| in the residuals " as result %8.4f `mr'
    if (`mr' > 0) {
        di as text "  reduction factor " as result %6.1f `=`ml'/`mr''
    }
    if (`mr' > 0.2) {
        di as text "  {err:Still substantial}.  The country" ///
           " models are not conditionally"
        di as text "  independent and the generalized responses are not" ///
                   " trustworthy."
    }
    di ""

    * ---- 4. residual diagnostics -------------------------------------------
    di as text "{hline 74}"
    di as text "  {bf:4. Are the residuals well behaved?}" _col(60) ///
               "{it:gvar diag}"
    di as text "{hline 74}"
    qui gvar diag, psc(`psc') nosummary
    local ne  = r(nequations)
    di as text "  serial correlation  " as result r(nsc)  as text "/" ///
       as result `ne' as text "   non-normal " as result r(njb) as text "/" ///
       as result `ne' as text "   ARCH " as result r(narch) as text "/" ///
       as result `ne'
    local scp = 100 * r(nsc) / max(`ne', 1)
    if (`scp' > 15) {
        di as text "  Serial correlation in " as result %4.1f `scp' ///
           as text "% of equations suggests the lag"
        di as text "  orders are too short.  See {bf:gvar lags} and" ///
                   " {bf:gvar diag, multivariate}."
    }
    di as text "  Non-normality is the usual finding in quarterly macro data"
    di as text "  and does not invalidate the generalized responses, which"
    di as text "  need only the second moments."
    di ""

    * ---- 5. the long-run relations -----------------------------------------
    if ("`sv'" == "1") {
        di as text "{hline 74}"
        di as text "  {bf:5. Do the long-run relations settle back?}" ///
                   _col(62) "{it:gvar pp}"
        di as text "{hline 74}"
        qui gvar pp, step(`step') nosummary
        local nr = r(nrelations)
        local ns = r(nslow)
        di as text "  " as result `nr' as text " cointegrating relations, " ///
           as result `ns' as text " still above 0.10 at horizon " ///
           as result `step' as text "."
        if (`ns' > 0) {
            di as text "  Those relations are not settling back, which points"
            di as text "  to an overstated rank.  Re-examine with" ///
                       " {bf:gvar coint}."
        }
        else {
            di as text "  Every profile decays as a long-run relation should."
        }
        di ""
    }

    * ---- 6. what to do next -------------------------------------------------
    di as text "{hline 74}"
    di as text "  {bf:Reading order}"
    di as text "{hline 74}"
    di as text "  {bf:gvar diag, multivariate reps(200)}   system-wide," ///
               " bootstrap p-values"
    di as text "  {bf:gvar stability, reps(250) shuffle}   parameter" ///
               " stability"
    di as text "  {bf:gvar irf, shock() reps(200) shuffle} responses with bands"
    di as text "  {bf:gvar spillover, by(unit)}            connectedness"
    di as text "  {help gvar_methods:gvar methods}         what the numbers mean"
    di ""

    return scalar wetest_rej = `nrej'
    return scalar wetest_tot = `ntot'
    return scalar corr_lev   = `ml'
    return scalar corr_res   = `mr'
    return scalar nequations = `ne'
end

* ---------------------------------------------------------------------------
mata:
void gvar_reportcorr(real matrix A)
{
    real scalar i, nl, nr, sl, sr

    nl = nr = sl = sr = 0
    for (i = 1; i <= rows(A); i++) {
        if (A[i, 3] < .) {
            sl = sl + abs(A[i, 3]); nl = nl + 1
        }
        if (A[i, 5] < .) {
            sr = sr + abs(A[i, 5]); nr = nr + 1
        }
    }
    st_numscalar("r(mlev)", sl / max((nl, 1)))
    st_numscalar("r(mres)", sr / max((nr, 1)))
}
end
* ---------------------------------------------------------------------------
* The specification dashboard (inventory 13.23).
*
* One page holding the six checks a referee asks for, in the order the audit
* prints them.  Each panel is produced by the subcommand that owns it -- this
* program computes nothing, it only arranges.  That matters: a dashboard that
* recomputed its own numbers could disagree with the tables beside it.
*
* Panels are built into temporary named graphs, combined, and dropped.  A
* panel that cannot be produced is SKIPPED and named in a note, rather than
* aborting the page: on an unsolved model four of the six still mean something.
*
* The whole panel list is resolved BEFORE any panel is built.  Validating
* inside the build loop left a temporary graph behind whenever a later name
* was bad, and made a typo cost a full eigen-decomposition first.  Every exit
* path from here drops what it created.
*
* graph combine rejects bgcolor(), so the shared region style cannot be
* passed to it wholesale -- only graphregion() is given here.
* ---------------------------------------------------------------------------
program define _gvar_report_dash, rclass
    version 14.0
    args gname psc step panels

    if ("`gname'" == "") local gname gvar_dashboard
    if ("`panels'" == "") local panels "solve wetest avgcorr diag pp unitroot"

    mata: st_local("sv", strofreal(gvar_issolved()))

    * ---- pass 1: resolve every name, build nothing --------------------------
    * name -> command map.  Kept as a table so adding a panel is one line and
    * so the skip message can name the subcommand the reader should run.
    local todo   ""
    local failed ""
    foreach p of local panels {
        local cmd ""
        if ("`p'" == "solve")     local cmd "gvar solve, graph nosummary"
        if ("`p'" == "wetest")    local cmd "gvar wetest, graph nosummary"
        if ("`p'" == "avgcorr")   local cmd "gvar avgcorr, graph nosummary"
        if ("`p'" == "diag")      local cmd "gvar diag, psc(`psc') graph nosummary"
        if ("`p'" == "pp")        local cmd "gvar pp, step(`step') graph nosummary"
        if ("`p'" == "coint")     local cmd "gvar coint, graph nosummary"
        if ("`p'" == "stability") local cmd "gvar stability, graph nosummary"
        if ("`p'" == "unitroot") {
            local cmd "gvar unitroot, domestic blocks(1) graph gstat(adf) gblock(1) nosummary"
        }
        if ("`cmd'" == "") {
            di as err "unknown dashboard panel: `p'"
            di as err "choose from: solve wetest avgcorr diag pp unitroot" ///
                      " coint stability"
            exit 198
        }

        * solve, pp and stability all need a solved system; asking for them on
        * an unsolved model is a skip, not an error
        if (("`p'" == "solve" | "`p'" == "pp" | "`p'" == "stability") ///
            & "`sv'" != "1") {
            local failed "`failed' `p'(not solved)"
            continue
        }
        local todo "`todo' `p'"
    }

    * ---- pass 2: build ------------------------------------------------------
    local built ""
    local np 0
    foreach p of local todo {
        local cmd ""
        if ("`p'" == "solve")     local cmd "gvar solve, graph nosummary"
        if ("`p'" == "wetest")    local cmd "gvar wetest, graph nosummary"
        if ("`p'" == "avgcorr")   local cmd "gvar avgcorr, graph nosummary"
        if ("`p'" == "diag")      local cmd "gvar diag, psc(`psc') graph nosummary"
        if ("`p'" == "pp")        local cmd "gvar pp, step(`step') graph nosummary"
        if ("`p'" == "coint")     local cmd "gvar coint, graph nosummary"
        if ("`p'" == "stability") local cmd "gvar stability, graph nosummary"
        if ("`p'" == "unitroot") {
            local cmd "gvar unitroot, domestic blocks(1) graph gstat(adf) gblock(1) nosummary"
        }

        local ++np
        local gn "_gvar_dash`np'"
        capture graph drop `gn'
        * capture already suppresses output; quietly inside it would let
        * -display as error- through, which is how the audit's own red
        * emphasis used to escape nosummary
        capture `cmd' name(`gn')
        if (_rc) {
            local failed "`failed' `p'(rc=`=_rc')"
            local --np
            continue
        }
        * a subcommand can return 0 and still draw nothing -- for instance a
        * scan in which every statistic is missing.  Confirm the graph exists
        * before it goes into the combine list.
        capture graph describe `gn'
        if (_rc) {
            local failed "`failed' `p'(no graph)"
            local --np
            continue
        }
        local built "`built' `gn'"
    }

    local nb : word count `built'
    if (`nb' == 0) {
        di as err "no dashboard panel could be produced."
        exit 459
    }

    * 1 panel -> 1 column, 2 -> 2, 3 or 4 -> 2, 5 or 6 -> 3.  Rows follow.
    local cols 3
    if (`nb' <= 2) local cols `nb'
    if (`nb' == 3 | `nb' == 4) local cols 2

    local nte "each panel is produced by the subcommand named in its title"
    if ("`failed'" != "") local nte "`nte'; skipped:`failed'"

    * NOT xcommon/ycommon.  The unit-circle panel has x in [-1,1] and the
    * diagnostic scans have x = 1..K; a common axis would flatten both into
    * unreadability.  Panels here are different measurements, not facets.
    capture noisily graph combine `built' ///
        , cols(`cols') iscale(0.62) ///
          graphregion(color(white) lwidth(none)) ///
          title("GVAR specification dashboard", size(medium) color(black)) ///
          note("`nte'", size(vsmall)) ///
          name(`gname', replace)
    local crc = _rc

    * drop the temporaries whether the combine worked or not
    foreach gn of local built {
        capture graph drop `gn'
    }
    if (`crc') exit `crc'

    di as text "  dashboard saved as " as result "`gname'" as text ///
       "  (" as result `nb' as text " panel(s)" _continue
    if ("`failed'" != "") di as text ", skipped: {err:`failed'}" _continue
    di as text ")"

    return local panels "`built'"
    return local skipped "`failed'"
    return scalar npanels = `nb'
end