*! version 1.0.2  19jul2024
*! Sebastian Kripfganz, www.kripfganz.de

*==================================================*
****** Panel data serial correlation testing ******

*** version history at the end of the file ***

program define xtdpdserial, rclass prop(xt)
	version 13.0
	_xt, treq
	syntax [varname(default=none num ts)] [if] [in] [, Statistics(string) noResiduals *]

	if "`varlist'" == "" {
		if "`residuals'" != "" {
			di as err "option noresiduals not allowed"
			exit 198
		}
		if "`e(cmd)'" == "" {
			error 301
		}
		if "`e(cmd)'" == "regress" {
			if "`e(absvar)'" != "" {
				di as err "requested action not valid after regress with option absorb()"
				exit 321
			}
		}
		else if inlist("`e(cmd)'", "xtdpdbc", "xtdpdgmm", "xtreg") {
			if "`e(cmd)'" == "xtreg" {
				if !inlist("`e(model)'", "fe", "re") {
					di as err "requested action not valid after xtreg with option `e(model)'"
					exit 321
				}
				if "`e(absvar)'" != "" & "`e(absvar)'" != "`e(ivar)'" {
					di as err "requested action not valid after xtreg with option absorb()"
					exit 321
				}
			}
		}
		else {
			error 321
		}
	}
	if `"`statistics'"' == "" & `"`options'"' == "" {
		loc statistics		"pm sdc dc fullc"
	}
	else if `"`statistics'"' != "" & `"`options'"' != "" {
		exit 198
	}
	if `"`statistics'"' == "" {
		xtdpdserial_stat `varlist' `if' `in', `residuals' `options'
	}
	else {
		xtdpdserial_parse_stats , `statistics'
		loc J				= `s(statnum)'
		loc statistics		`"`s(stats)'"'
		forv j = 1 / `J' {
			loc options`j'		`"`s(stat`j')'"'
		}
		tempname S
		forv j = 1 / `J' {
			cap noi xtdpdserial_stat `varlist' `if' `in', `residuals' `options`j'' `options'
			if "`r(chi2)'" != "" {
				ret loc chi2_`j'	= r(chi2)
				ret loc df_`j'		= r(df)
				ret loc p_`j'		= r(p)
			}
		}
	}
end

program define xtdpdserial_stat, rclass prop(xt)
	version 13.0
	syntax [varname(default=none num ts)] [if] [in] [, noResiduals *]
	marksample touse

	*--------------------------------------------------*
	*** syntax parsing of options ***
	xtdpdserial_parse_options , `options'
	loc collapse		= `s(collapse)'
	loc difference		= `s(difference)'
	loc forward			= `s(forward)'
	loc backward		= `s(backward)'
	loc order			`"`s(order)'"'
	loc lagrange		`"`s(lagrange)'"'
	loc ts				`"`s(ts)'"'
	loc label			"`s(label)'"

	*--------------------------------------------------*
	*** estimation results ***
	tempvar e
	if "`varlist'" == "" {
		qui replace `touse' = 0 if !e(sample)
		if "`e(cmd)'" == "xtreg" & "`e(model)'" == "re" {
			qui predict double `e' if `touse', ue
			loc varlist			"`e'"
		}
		else {
			if "`e(cmd)'" == "regress" {
				qui predict double `e' if `touse', r
			}
			else {
				qui predict double `e' if `touse', ue
			}

			tempname b
			mat `b'				= e(b)
			loc indepvars		: coln `b'
			loc indepvars		: subinstr loc indepvars "_cons" "`touse'", w c(loc constant)
			if !`constant' {
				loc indepvars		: subinstr loc indepvars "o._cons" "o.`touse'", w c(loc constant)
			}
			loc K				: word count `indepvars'

			if inlist("`e(cmd)'", "regress", "xtreg") {
				if e(N_clust) < . {
					loc q				= sqrt(e(N_clust) / (e(N_clust) - 1) * (e(N) - 1) / (e(N) - e(rank)))
				}
				else {
					loc q				= sqrt(e(N) / (e(N) - e(rank)))
				}
			}
			if "`e(cmd)'" == "regress" {
				forv k = 1 / `K' {
					tempvar score`k'
					qui gen double `score`k'' = `q' * `: word `k' of `indepvars'' * `e' if `touse'
					loc scorevars		"`scorevars' `score`k''"
				}
			}
			else if "`e(cmd)'" == "xtreg" {
				tempvar score
				qui predict double `score' if `touse', e
				forv k = 1 / `K' {
					tempvar score`k'
					loc var				: word `k' of `indepvars'
					if "`var'" == "`touse'" {
						qui gen double `score`k'' = `q' * `var' * `e' if `touse'
					}
					else {
						qui gen double `score`k'' = `q' * `var' * `score' if `touse'
					}
					loc scorevars		"`scorevars' `score`k''"
				}
			}
			else {
				forv k = 1 / `K' {
					tempvar score`k'
					loc scorevars		"`scorevars' `score`k''"
				}
				qui predict double `scorevars' if `touse', sc
			}
			cap conf mat e(V_modelbased)
			if !_rc {
				loc V				"e(V_modelbased)"
			}
			else if "`e(cmd)'" == "regress" {
				tempname V
				mat `V'				= (e(N) - e(rank)) / e(rss) * e(V)
			}
			else if "`e(cmd)'" == "xtreg" {
				tempname V
				mat `V'				= e(V) / e(sigma_e)^2
			}
			else {
				loc V				"e(V)"
			}
		}
	}
	else {
		qui gen double `e'	= `varlist' if `touse'
		if "`residuals'" == "" {
			tempvar resid
			foreach res in ue e resid {
				cap predict `resid' if `touse', `res'
				if !_rc {
					cap _rmdcoll `e' `resid'
					if _rc {
						di as err "test not valid for regression residuals"
						exit 459
					}
				}
				cap drop `resid'
			}
		}
		else {
			di as txt "note: test not valid for regression residuals"
		}
	}
	tempvar dtouse
	qui gen byte `dtouse' = `touse'
	markout `dtouse' D.`e'

	*--------------------------------------------------*
	*** lag order ***
	if "`ts'" != "" {
		cap tsrevar `ts'.`e' if `touse'
		if _rc {
			di as err "option ts() incorrectly specified"
			exit 198
		}
	}
	if `backward' {
		if "`ts'" == "" {
			sum `_dta[_TStvar]' if `touse', mean
			loc maxlag			= (r(max) - r(min)) / `_dta[_TSdelta]'
			if `maxlag' < 2 + (`difference' != 0) {
				error 2001
			}
			xtdpdserial_lagrange , maxlag(`maxlag') difference(`difference') `lagrange' `order'
			loc ts				"`s(ts)'"
			loc order			= `s(order)'
		}
		loc eL				"`ts'.`e'"
	}
	else if "`ts'" == "" {
		loc order			= 2
	}
	if `forward' {
		loc eF				"F.`e'"
	}
	if "`varlist'" == "" {
		foreach var of loc indepvars {
			if `backward' {
				if "`var'" != "`touse'" {
					if `difference' {
						_ms_parse_parts `var'
						if "`r(type)'" != "variable" | "`r(op)'" != "" {
							fvrevar `var'
							loc var				"`r(varlist)'"
						}
					}
					fvrevar `ts'.`var' if `touse'
					loc indepL			"`indepL' `r(varlist)'"
				}
				else {
					loc indepL			"`indepL' `ts'.`touse'"
				}
			}
			if `forward' {
				fvrevar F.`var' if `touse'
				loc indepF			"`indepF' `r(varlist)'"
			}
		}
	}

	*--------------------------------------------------*
	*** serial correlation test ***
	tempname serial
	mata: `serial' = xtdpdserial_init()
	mata: xtdpdserial_init_touse(`serial', "", "`touse'")
	mata: xtdpdserial_init_touse(`serial', "diff", "`dtouse'")
	mata: xtdpdserial_init_varname(`serial', "", "`e'")
	if `backward' {
		mata: xtdpdserial_init_varname(`serial', "L", "`eL'")
	}
	if `forward' {
		mata: xtdpdserial_init_varname(`serial', "F", "`eF'")
	}
	if "`varlist'" == "" {
		mata: xtdpdserial_init_indepvars(`serial', "", "`indepvars'")
		if `backward' {
			mata: xtdpdserial_init_indepvars(`serial', "L", "`indepL'")
		}
		if `forward' {
			mata: xtdpdserial_init_indepvars(`serial', "F", "`indepF'")
		}
		mata: xtdpdserial_init_scorevars(`serial', "`scorevars'")
		mata: xtdpdserial_init_covmat(`serial', st_matrix("`V'"))
	}
	mata: xtdpdserial_init_by(`serial', "`_dta[_TSpanel]'")
	mata: xtdpdserial_init_time(`serial', "`_dta[_TStvar]'")
	if `collapse' {
		mata: xtdpdserial_init_collapse(`serial', `collapse')
	}
	mata: xtdpdserial(`serial')
	mata: st_numscalar("r(chi2)", xtdpdserial_result_chi2(`serial'))
	mata: st_numscalar("r(df)", xtdpdserial_result_rank(`serial'))
	loc chi2			= r(chi2)
	loc df				= r(df)
	loc p				= chi2tail(`df', `chi2')

	*--------------------------------------------------*
	*** display of test results ***
	di _n as txt "`label'" _c
	di _col(56) "chi2(" as res `df' as txt ")" _col(68) "=" _col(70) as res %9.4f `chi2'
	di as txt "H0: no autocorrelation " _c
	if `difference' < 3 {
		if `order' == . {
			di "of any order" _c
		}
		else {
			di "up to order `order'" _c
		}
	}
	di _col(56) as txt "Prob > chi2" _col(68) "=" _col(73) as res %6.4f `p'

	*--------------------------------------------------*
	*** returned test results ***
	ret sca p			= `p'
	ret sca df			= `df'
	ret sca chi2		= `chi2'
end

*==================================================*
**** syntax parsing of tests ****
program define xtdpdserial_parse_stats, sclass
	version 13.0
	syntax , [*]

	loc statistics		`"`options'"'
	loc j				= 0
	while `"`statistics'"' != "" {
		gettoken stat statistics : statistics, bind
		loc ++j
		loc collapse		""
		loc stats			`"`stats' `stat'"'
		gettoken stat order : stat, p("(") bind
		if `"`order'"' != "" {
			loc 0				`", order`order'"'
			syntax , Order(numlist max=2)
			if `: word count `order'' == 1 {
				loc order			`"order(`order')"'
			}
			else {
				loc order			`"lagrange(`order')"'
			}
		}
		loc stat			: list retok stat
		if `"`stat'"' != "pm" & `"`stat'"' != "fullc" {
			gettoken stat collapse : stat, p("c") bind
			loc stat			: list retok stat
			loc label			""
		}
		cap xtdpdserial_parse_options , `stat' `collapse' `order'
		if _rc {
			di as err "option statistics() incorrectly specified"
			exit 198
		}
		sret loc stat`j'	`"`stat' `collapse' `order'"'
	}
	sret loc statnum	= `j'
	sret loc stats		`"`stats'"'
end

*==================================================*
**** syntax parsing of options ****
program define xtdpdserial_parse_options, sclass
	version 13.0
	syntax , [PM Difference SDifference Order(passthru) Lagrange(passthru) Collapse FULLCollapse noForward noBackward TS(string)]

	if `"`order'"' != "" & `"`lagrange'"' != "" {
		di as err "options order() and lagrange() may not be combined"
		exit 184
	}
	if "`collapse'" != "" & "`fullcollapse'" != "" {
		di as err "options collapse and fullcollapse may not be combined"
		exit 184
	}
	loc collapse		= ("`collapse'" != "") + 2 * ("`fullcollapse'" != "")
	if "`ts'" != "" {								// undocumented expert option (might result in invalid tests if used incorrectly)
		if "`difference'`sdifference'`forward'`backward'" != "" {
			di as err "options ts() and `: word 1 of `difference' `sdifference' `forward' `backward'' may not be combined"
			exit 184
		}
		if `"`order'"' != "" {
			di as err "options ts() and order() may not be combined"
			exit 184
		}
		if `"`lagrange'"' != "" {
			di as err "options ts() and lagrange() may not be combined"
			exit 184
		}
		loc difference		= 3
		loc forward			= 0
		loc backward		= 1
	}
	else if "`difference'" != "" {
		if "`sdifference'`backward'" != "" {
			di as err "options difference and `: word 1 of `sdifference' `backward'' may not be combined"
			exit 184
		}
		loc difference		= 1
		loc forward			= 0
		loc backward		= 1
		loc label			"test in first differences"
	}
	else if "`sdifference'" != "" {
		if "`forward'`backward'" != "" {
			di as err "options sdifference and `: word 1 of `forward' `backward'' may not be combined"
			exit 184
		}
		loc difference		= 2
		loc forward			= 0
		loc backward		= 1
		loc label			"test in seasonal differences"
	}
	else {
		if "`forward'" != "" & "`backward'" != "" {
			di as err "options noforward and nobackward may not be combined"
			exit 184
		}
		loc difference		= 0
		loc forward			= ("`forward'" == "")
		loc backward		= ("`backward'" == "")
		if `forward' & `backward' {
			loc label			"portmanteau test"
		}
		else {
			loc label			"restricted portmanteau test"
		}
	}
	if `collapse' == 1 {
		loc label			"collapsed `label'"
	}
	else if `collapse' == 2 {
		loc label			"fully-collapsed `label'"
	}

	sret loc collapse	= `collapse'
	sret loc difference	= `difference'
	sret loc forward	= `forward'
	sret loc backward	= `backward'
	sret loc order		`"`order'"'
	sret loc lagrange	`"`lagrange'"'
	sret loc ts			`"`ts'"'
	sret loc label		"`label'"
end

*==================================================*
**** determination of lag range ****
program define xtdpdserial_lagrange, sclass
	version 13.0
	syntax , MAXLag(integer) Difference(integer) [Lagrange(numlist max=2 int miss >1) Order(numlist max=1 int miss >1)]

	if "`order'" == "" {
		loc order			= .
	}
	else {
		if `difference' == 1 & `order' < . {
			loc --order
		}
		if `order' == 1 {
			di as err "order() invalid -- invalid numlist has elements outside of allowed range"
			exit 125
		}
		if `order' > `maxlag' {
			loc order			= `maxlag'
		}
		loc lagrange		"2 `order'"
		if `difference' == 1 {
			loc ++order
		}
	}
	if `difference' == 1 {
		loc --maxlag
	}
	if "`lagrange'" != "" {
		gettoken lag1 lag2 : lagrange
		if `lag1' == . {
			loc lag1			= 2
		}
		if "`lag2'" == "" {
			loc lag2			= `lag1'
		}
		else if `lag2' == . {
			loc lag2			= `maxlag'
		}
		if `lag1' > `lag2' {
			di as err "lagrange() invalid -- invalid numlist has elements out of order"
			exit 124
		}
		else if `lag1' > `maxlag' {
			error 2001
		}
		else if `lag2' > `maxlag' {
			loc lag2			= `maxlag'
			loc order			= `maxlag' + (`difference' == 1)
		}
		else if `order' == . {
			loc order			= `lag2'
			if `difference' == 1 {
				loc ++order
			}
		}
		if `difference' == 2 {
			loc ++lag1
			loc ++lag2
		}
	}
	else if `difference' == 2 {
		loc lag1			= 3
		loc lag2			= `maxlag' + 1
	}
	else {
		loc lag1			= 2
		loc lag2			= `maxlag'
	}
	loc lagrange		"`lag1'/`lag2'"
	if `difference' == 1 {
		loc ts				"L(`lagrange')D"
	}
	else if `difference' == 2 {
		loc ts				"FS(`lagrange')"
	}
	else {
		loc ts				"L(`lagrange')"
	}

	sret loc lag1		= `lag1'
	sret loc lag2		= `lag2'
	sret loc order		= `order'
	sret loc ts			"`ts'"
end

*==================================================*
*** version history ***
* version 1.0.2  19jul2024  bug fixed with fully collapsed tests in unbalanced panels
* version 1.0.1  12jul2024  support added for xtreg, re
* version 1.0.0  11jul2024  available online at www.kripfganz.de
* version 0.0.1  02jul2024