niedziela, 25 czerwca 2023

Jak sprawdzić stabilność każdego współczynnika osobno (język R)?

Kontynuuję temat analizy stabilności w języku R. Jest to kontynuacja serii 1, 2, 3, 4, 5. Dotychczas pokazywałem sposób testowania stabilności całego modelu. Był to dobry sposób, aby sprawdzić czy i w którym miejscu zmienił się wyraz wolny lub współczynnik kierunkowy, czyli wpływ zmiennej objaśniającej. Jeżeli jednak model zawierał zarówno wyraz wolny jak i współczynnik kierunkowy, a chciałem ocenić co dokładnie i kiedy się zmieniło, zaczynały się problemy. Dotychczas radziłem sobie w ten sposób, że testowałem stałość średniej poprzez test stacjonarności zmiennych i jeśli została ona zachowana, dopiero testowałem stabilność modelu. Jeśli model okazał się niestabilny, wnioskowałem, że współczynnik kierunkowy się zmienił, ponieważ wyraz wolny pełnił rolę średniej zmiennej objaśnianej (która z testu stacjonarności okazała się stała). To rozwiązanie jednak odpada, jeśli model zawiera wiele zmiennych objaśniających - nie będę wiedział, która zmienna "zmieniła" współczynnik.

Będą potrzebne 2 pakiety: strucchange oraz forecast. Jednak postanowiłem włączyć do swoich analiz trzeci pakiet, xts. 

Czasem może się też przydać zmiana opcji programu, ponieważ wolę unikać notacji naukowej. Często dostajemy małe liczby, np. 0.0001. Normalnie program wyświetli 1e-04. Żeby dostać zapis dziesiętny, moża użyć funkcji options(scipen = 1). Jednak jeszcze mniejsza liczba, jak 0.00001 znów zostanie zapisana naukowo. Żeby przywrócić zapis dziesiętny, musimy zwiększyć scipen do 2. Itd. Domyślne ustawienia to scipen = 0. Czyli jeśli część dziesiętna ma powyżej 3 zer, to zwiększamy scipen o odpowiednio dużą liczbę. Wystarczy, że ustawimy scipen = 100. Trzeba zauważyć, że nie chodzi tu tylko o część dziesiętną, ale ogólnie o liczbę cyfr. Dla scipen = 100 dopiero po wystąpieniu powyżej 103 cyfr w całej liczbie dostaniemy notację naukową. 

Dodatkowym problemem jest nie rzadko wyświetlanie wielu cyfr w części dziesiętnej. Chodzi tu o format, a nie zaokrąglenie. Aby kontrolować ten format stosujemy parametr digits w funkcji options. Domyślnie program używa digits = 7, co oznacza, że maksymalnie wyświetli się 7 cyfr. Np. liczbę 555.00001 pokaże jako 555. Aby zwiększyć precyzję, musimy ustawić digits = 8 lub więcej. W naszym przypadku lepiej będzie na odwrót - zmniejszymy precyzję, ponieważ nasze liczby to zazwyczaj ułamki dziesiętne i nie potrzebujemy aż 7 cyfr po kropce czy przecinku. Dla obecnych potrzeb ustawię digits = 4. 

Dla przypomnienia kody dzielę zazwyczaj na 3 części:

1. Przedwstęp - najbardziej wstępne ustawienia, nie trzeba nic zmieniać

2. Wstęp - ustawianie ścieżek i modelu, tutaj wprowadzamy własne dane

3. Część główna - dotyczy głównego tematu, zazwyczaj nie trzeba nic zmieniać.

Z powodu takiego podziału przykłady zawierają jedynie Wstęp i Część główną.

# Przedwstęp

#precyzja i sposób zapisu liczb: 

options(scipen = 100, digits = 4)

#pakiety

if (require("strucchange")==FALSE) {

  install.packages("strucchange")

  library("strucchange")

}

if (require("forecast")==FALSE) {

  install.packages("forecast")

  library("forecast")

}

if (require("xts")==FALSE) {

  install.packages("xts")

  library("xts") 

}

Pakiet strucchange jest bardzo bogaty w testy stabilności. Dotychczas stosowałem testy bazujące na statystyce F. Pakiet jednak oferuje także drugą grupę testów - fluktuacji procesów empirycznych (The Empirical Process, EFP). Pierwsza grupa może wskazać co najwyżej momenty zmiany struktury modelu. Druga grupa w pewnym sensie jest bardziej ogólna, bo pokazuje jak dochodzi do tej zmiany i co się dzieje po tej zmianie - punkt zostaje zastąpiony przedziałem w czasie. Ma to duże znaczenie dla zrozumienia metodologii i interpretacji tego typu testów. W teście F dzielono próbę na dwie (różne) części i sprawdzano czy gdzieś w modelu empirycznym następuje zmiana. Tutaj natomiast sprawdza się zachowanie modelu w różnych przedziałach próby na tle procesu czysto losowego. W stabilnym modelu empiryczna ocena parametru zawsze losowo fluktuuje wokół teoretycznego parametru. Jeśli jej wariancja staje się za duża, znaczy, że proces przestaje być losowy. Aby określić czy mamy do czynienia ciągle z fluktuacją losową, wykorzystuje się centralne twierdzenie graniczne funkcjonałów. Zgodnie z tym twierdzeniem suma wielu zmiennych niezależnych ze stałą średnią i wariancją będzie w (dodatkowym) wymiarze czasowym błądzeniem przypadkowym. Dlatego w teście sumowane są kolejne odchylenia od parametru teoretycznego w danym przedziale próbnym i sprawdza się czy to zsumowane odchylenie tworzy błądzenie przypadkowe. Jeśli fluktuacja jest za duża i wychodzi poza błądzenie przypadkowe, to znaczy, że wartość prawdziwego parametru się zmieniła. Pełen zakres czasu wynosi (0, 1, ... t, ... T). Kolejne przedziały testowania są budowane na 2 sposoby, (a) od 0 do t albo (b) od t-h do t+h. Przedział (a) dotyczy skumulowanych sum reszt standardowych (ang. cumulative sums of standardized residuals, CUSUM) oraz estymacji rekurencyjnych (recursive estimates, RE), natomiast (b) ruchomych sum reszt (moving sums of residuals, MOSUM) oraz estymacji ruchomych (moving estimates, ME). W przypadku RE i ME odchylenia dotyczą nie tyle sum, co średnich w danym przedziale. Wszystkie te testy zostały powiązane w jedną klasę tzw. uogólnionego testu fluktuacji (The Generalized Fluctuation Test [1]), a ich procesy w uogólniony empiryczny proces fluktuacji (Generalized Empirical Fluctuation Process, GEFP).
Z moich obserwacji wynika, że nie ma najlepszego testu fluktuacji. Dla mniejszych prób ME najlepiej radzi sobie z odkrywaniem nagłych zmian w symulowanych modelach. Żeby mieć pełen obraz należy wykonać wszystkie testy i dopiero wtedy wyciągać wnioski.
Są różne typy testu CUSUM. Standardowy test tego rodzaju opisałem już kiedyś w tym art., gdy swoje badania wykonywałem w gretlu. Nas interesują tylko te, które umożliwiają analizę poszczególnych parametrów. Są to Score-CUSUM, Score-MOSUM, ME, RE i GEFP.
Pierwsze próby wykonam na symulacjach, aby sprawdzić jak test sobie radzi. Symulowana próba składa się z 1500 danych podzielona na 3 identyczne części. Najpierw zrobię przykład stabilnego ARMA(1,1).

Przykład 1. Brak zmian: ARMA(1, 1)

#Wstęp
set.seed(1980)

sim1 = arima.sim(list(order=c(1,0,1), ar=0.6, ma=0.3), n=500, mean=1)
sim2 = arima.sim(list(order=c(1,0,1), ar=0.6, ma=0.3), n=500, mean=1) 
sim3 = arima.sim(list(order=c(1,0,1), ar=0.6, ma=0.3), n=500, mean=1)

sim123 = ts(c(sim1, sim2, sim3))

#tworzenie modelu arma(1,1)
sim = ts(sim123[-1])
sim_ar1 = ts(sim123[-length(sim123)])
sim_arma11 = arima(sim123, order=c(1,0,1))
sim_ma = ts(residuals(sim_arma11))
sim_ma1 = ts(sim_ma[-length(sim_ma)])
sim.model <- sim ~ sim_ar1 + sim_ma1


# Część główna

plot(sim)



Są dwie funkcje do użytku: efp() i gefp(). 

# EFP: CUSUM, MOSUM, ME, RE

# jest wiele podtypów testu CUSUM, związane metodą estymacji: OLS, Recursive CUSUM, Score-CUSUM. Tylko ten ostatni pozwala przeanalizować wszystkie współczynniki na wykresie. Podobnie jest z MOSUM.

#Score based CUSUM
# Sumarycznie:
cus = efp(sim.model, type="Score-CUSUM")
plot(cus, functional="max")


# Osobno dla każdego parametru:

cus = efp(sim.model, type="Score-CUSUM")
plot(cus, functional=NULL)



Aby zobaczyć zmiany w każdym parametrze, trzeba użyć functional=NULL.
Zwróćmy uwagę, że testujemy tutaj także wariancję modelu. 

#Score based MOSUM
mos = efp(sim.model, type="Score-MOSUM")
plot(mos, functional="max")



MOSUM wskazał błędnie zmianę struktury modelu - została przebita linia czerwona. Zobaczmy, który czynnik wywołał taki błąd:

plot(mos, functional=NULL)



A więc test błędnie wskazał na zmianę struktury w wariancji modelu. 

#Moving estimate
me <- efp(sim.model, type="ME")
plot(me, functional="max")



plot(me, functional = NULL)



#recursive estimate
par(mfrow=c(3,1), mar=c(2.5,5,1.5,3))
re <- efp(sim.model, type="RE")
plot(re, functional="max")


plot(re, functional = NULL)


#GEFP
par(mfrow=c(3,1), mar=c(2.5,5,1.5,3))
gcus = gefp(sim.model)
plot(gcus, aggregate=TRUE)



plot(gcus, aggregate=FALSE)


Wszystkie testy, za wyjątkiem Score-MOSUM, dały prawidłową odpowiedź.


Przykład 2. Zmiana wyrazu wolnego ARMA(1, 1)

#Wstęp

set.seed(1980)

sim1 = arima.sim(list(order=c(1,0,1), ar=0.6, ma=0.3), n=500, mean=1)

sim2 = arima.sim(list(order=c(1,0,1), ar=0.6, ma=0.3), n=500, mean=0) 

sim3 = arima.sim(list(order=c(1,0,1), ar=0.6, ma=0.3), n=500, mean=1)

sim123 = ts(c(sim1, sim2, sim3))

#tworzenie modelu
sim = ts(sim123[-1])

sim_ar1 = ts(sim123[-length(sim123)])

sim_arma11 = arima(sim123, order=c(1,0,1))

sim_ma = ts(residuals(sim_arma11))

sim_ma1 = ts(sim_ma[-length(sim_ma)])

plot(sim)



#Część główna
# CUSUM, MOSUM, ME, RE, GEFP

par(mfrow=c(3,1), mar=c(2.5,5,1.5,3))

#Score based CUSUM
cus = efp(sim.model, type="Score-CUSUM")
plot(cus, functional="max")




Przebicie linii czerwonej dowodzi, że CUSUM prawidłowo rozpoznał dwukrotną zmianę struktury. Sprawdźmy, który czynnik wywołał zmianę.

plot(cus, functional=NULL)

Faktycznie wyraz wolny dwukrotnie wystaje poza losowy obszar, więc CUSUM prawidłowo rozpoznaje niestabilność.


#Score based MOSUM
mos = efp(sim.model, type="Score-MOSUM")
plot(mos, functional="max")



MOSUM - prawidłowo, zróbmy analizę poszczególnych współczynników:

plot(mos, functional=NULL)


MOSUM zupełnie inaczej przedstawia sytuację od CUSUM. Wg MOSUM pierwsza część danych (sim1) jest "normalna", druga (sim2) wskazuje na zmienioną strukturę w całym zakresie, a trzecia (sim3) powrót do "normalności". Normalność wynika z tego, że więcej czasu (2/3) zajmuje w por. do "nienormalności" (1/3).

#Moving estimate
me <- efp(sim.model, type="ME")
plot(me, functional="max")



W agregatach prawidłowo.

plot(me, functional = NULL)



W dezagregatach ME wskazuje poprawnie, że zmienił się wyraz wolny, ale błędnie, że dodatkowo zmienił się wpływ danych z poprzedniego okresu. Jest to zastanawiające, bo 1500 obserwacji to duża próba i nie powinno być takich błędów.

#recursive estimate
re <- efp(sim.model, type="RE")
plot(re, functional="max")


re <- efp(sim.model, type="RE")
plot(re, functional = NULL)


RE także informuje, że nastąpiła dwukrotnie zmiana na plus i na minus tylko dla wyrazu wolnego - OK.


#GEFP
gcus = gefp(sim.model)
plot(gcus, aggregate=TRUE)



plot(gcus, aggregate=FALSE)


GEFP pokazał to samo co CUSUM i RE - prawidłowo.


W omówionym przykładzie najgorzej poradził sobie ME.  


Przykład 3. Zmiana z ARMA(1, 1) na ARMA(0, 1) i powrót do ARMA(1, 1)

#Wstęp

set.seed(1980)

sim1 = arima.sim(list(order=c(1,0,1), ar=0.6, ma=0.3), n=500, mean=1)
sim2 = arima.sim(list(order=c(1,0,1), ar=0.000001, ma=0.3), n=500, mean=1) 
sim3 = arima.sim(list(order=c(1,0,1), ar=0.6, ma=0.3), n=500, mean=1)
sim123 = ts(c(sim1, sim2, sim3))

#tworzenie modelu
sim = ts(sim123[-1])
sim_ar1 = ts(sim123[-length(sim123)])
sim_arma11 = arima(sim123, order=c(1,0,1))
sim_ma = ts(residuals(sim_arma11))
sim_ma1 = ts(sim_ma[-length(sim_ma)])

sim.model <- sim ~ sim_ar1 + sim_ma1

plot(sim)



#Część główna
# CUSUM, MOSUM, ME, RE, GEFP
#Score based CUSUM
par(mfrow=c(3,1), mar=c(2.5,5,1.5,3))
cus = efp(sim.model, type="Score-CUSUM")
plot(cus, functional="max")



plot(cus, functional=NULL)


CUSUM wprawdzie ogólnie wskazał dwa momenty zmiany, ale osobno raz pomylił wyraz wolny ze zmianą parametru AR(1), zaś dla drugiej zmiany pokazał prawidłowo. 

#Score based MOSUM
mos = efp(sim.model, type="Score-MOSUM")
plot(mos, functional="max")


plot(mos, functional=NULL)

MOSUM jeszcze gorzej od CUSUM - wskazał zmiany wyraz wolnego i MA(1). Część AR(1) tylko sygnalizowała zmianę.

#Moving estimate
me <- efp(sim.model, type="ME")
plot(me, functional="max")



 plot(me, functional = NULL)




ME tym razem pokazał prawidłowo zmianę par. AR(1), a wyraz wolny nie przebił linii czerwonej. 

#recursive estimate
re <- efp(sim.model, type="RE")
plot(re, functional="max")




plot(re, functional = NULL)



RE przynosi podobny efekt do CUSUM - błędnie informuje o zmianie wyrazu wolnego. 

#GEFP
gcus = gefp(sim.model)
plot(gcus, aggregate=TRUE)



plot(gcus, aggregate=FALSE)

W przypadku GEFP to samo co dla poprzednika.

Podsumowując ten przykład, ME dla odmiany okazał się tu najlepszy. 


Przykład 4. Zmiana części MA

Ponieważ proces MA(1) można wyrazić w postaci procesu AR(T), gdzie T to cały zakres próby (zob. ten wpis), a więc pozostaje częściowo zależny od AR(1), zmiana zachowania MA jest problematyczna w testach. Aby uzyskać efekt, obniżę 0,3 do -0,4. Prawidłowe połączenie prób wymaga wtedy trudniejszego zapisu, który analizowałem tutaj. Wtedy zapis wstępny będzie następujący:

#Wstęp

set.seed(1980)

dlugosc_proby = 500

sim1 = arima.sim(list(order=c(1,0,1), ar=0.6, ma=0.3), n=dlugosc_proby, mean=1, n.start=2)

sim2 = arima.sim(list(order=c(1,0,1), ar=0.6, ma=-0.4), n=dlugosc_proby, mean=1, start.innov=c(sim1[dlugosc_proby-1], sim1[dlugosc_proby]), n.start=2)

sim12 = c(sim1, sim2)

sim3 = arima.sim(list(order=c(1,0,1), ar=0.6, ma=0.3), n=dlugosc_proby, mean=1, start.innov=c(sim12[dlugosc_proby-1], sim12[dlugosc_proby]), n.start=2)

sim123 = ts(c(sim12, sim3))

#tworzenie modelu arma(1,1)

sim = ts(sim123[-1])

sim_ar1 = ts(sim123[-length(sim123)])

sim_arma11 = arima(sim123, order=c(1,0,1))

sim_ma = ts(residuals(sim_arma11))

sim_ma1 = ts(sim_ma[-length(sim_ma)])

sim.model <- sim ~ sim_ar1 + sim_ma1

plot(sim)




Od teraz kod będzie zawierał tylko wykres dla każdego współczynnika osobno.

#Część główna
# CUSUM, MOSUM, ME, RE, GEFP

#Score based CUSUM
par(mfrow=c(3,1), mar=c(2.5,5,1.5,3))
cus = efp(sim.model, type="Score-CUSUM")
plot(cus, functional=NULL)



CUSUM błędnie wskazuje na zmianę każdego współczynnika.

#Score based MOSUM
par(mfrow=c(3,1), mar=c(2.5,5,1.5,3))
mos = efp(sim.model, type="Score-MOSUM")
plot(mos, functional=NULL)


MOSUM wskazał małą (dwukrotną) zmianę wyrazu wolnego i sporą zmianę w MA. Kiedy spojrzymy na wykres, to zauważymy, że faktycznie wydaje się, jakby średnia dla sim2 spadła. W rzeczywistości nie spadła tylko utrzymuje się na poziomie 1,4, a to właśnie sim1 i sim3 mają za wysokie średnie: dla sim1 3,12, sim3 3,41. Nie ma tu błędu, a po prostu silne autokorelacje podwyższają średnią empiryczną. MOSUM obiektywnie więc prawidłowo informuje nas o zmianach na wykresie, bo główną przyczynę utraty stabilności dostrzega w części MA. Część AR nie przebiła linii czerwonej, a wyraz wolny ledwo się poza nią wychylił.


#Moving estimate
par(mfrow=c(3,1), mar=c(2.5,5,1.5,3))
me <- efp(sim.model, type="ME")
plot(me, functional = NULL)


ME z kolei błędnie widzi zmianę w AR, a niewiele zmian w MA.


#recursive estimate
par(mfrow=c(3,1), mar=c(2.5,5,1.5,3))
re <- efp(sim.model, type="RE")
plot(re, functional = NULL)



RE kompletnie zawodzi w tym przypadku.


#GEFP
par(mfrow=c(3,1), mar=c(2.5,5,1.5,3))
gcus = gefp(sim.model)
plot(gcus, aggregate=FALSE)


Podobnie jak CUSUM - błędnie.

W przykładzie 4 najlepszym okazał się MOSUM. I teraz idziemy z żywymi przykładami.


Przykład 5. WIG tygodniowy w korelacji z S&P 500. 
Okres: 02.01.2000 - 18.06.2023 (1225 obserwacji). Źródło: stooq.pl

Dla przypomnienia, ponieważ działam na Windowsie, używanie ścieżek w R nastręcza problemów, stąd kod przekształcający "\" w "/".
 

# Wstęp
#zamieniam "\" na "/"

sciezka = r"(C:\R\testy)"
sciezka = gsub("\\", "/", sciezka, fixed=T)

# albo
# sciezka = gsub("\\\\", "/", sciezka)
# ustawiamy folder roboczy

setwd(sciezka)

# pliki - źródło stooq.pl
nazwa1 = "^spx_w.csv"
nazwa2 = "wig_w.csv"

nazwa <- c(nazwa1, nazwa2)
dane <- list()
stopa <- list()


for (i in 1:2) {

if (grepl(";", readLines(nazwa[i], n=1))) {
plik = read.csv(nazwa[i], sep=";", dec=",")
} else if (grepl(",", readLines(nazwa[i], n=1))) {
plik = read.csv(nazwa[i], sep=",", dec=".")
} else {
stop("Unknown separator in the file: ", nazwa[i])
}

daty = as.Date(plik[,1], tryFormats = c("%d.%m.%Y", "%Y.%m.%d", "%d-%m-%Y", "%Y-%m-%d", "%d/%m/%Y", "%Y/%m/%d"))
daty = as.Date(daty)
#jeżeli ostatnia data przekracza wczorajszą datę, to usuwamy ostatnią obserwację

if (daty[length(daty)]>=Sys.Date()) {

plik = plik[-nrow(plik),]
daty = daty[-length(daty)]
}

# inne dla pliku:

st = TRUE
k = 5

zmienna = as.numeric(plik[,k])
dane[[i]] <- na.omit(data.frame(daty, zmienna))
dane[[i]] <- as.xts(dane[[i]])

# warunek czy stopy zwrotu
if (st==TRUE) {
stopa[[i]] = round(diff(as.numeric(dane[[i]][,1]))/as.numeric(dane[[i]][,1][-length(dane[[i]][,1])]), 4)
stopa[[i]] = as.xts(na.omit(data.frame(daty[-1], stopa[[i]])))
}

dane_merged <- merge.xts(dane[[1]], dane[[2]], join="inner")
stopa_merged <- merge.xts(stopa[[1]], stopa[[2]], join="inner")

colnames(dane_merged) <- c('SP500', 'WIG')
colnames(stopa_merged) <- c('SP500', 'WIG')

sp500 = dane_merged$SP500
wig = dane_merged$WIG
stopa_sp500 = stopa_merged$SP500
stopa_sp500_1 = stopa_sp500[-length(stopa_sp500)]
stopa_wig = stopa_merged$WIG

cor(wig, sp500)
cor(stopa_wig, stopa_sp500)

Korelacja między WIG a S&P500 wynosi 0,72, natomiast ich stopy zwrotu korelują na poziomie 0,51. Regresję liniową przy pomocy funkcji lm [linear model] zbudujemy na stopach zwrotu.

stopa.model <- lm(stopa_wig ~ stopa_sp500)
summary(stopa.model)


Model jest oczywiście istotny (wpływ SP500 wynosi 58%), natomiast ciekawe, że wyraz wolny jest nieistotny - średnia niewarunkowa stopa wychodzi zerowa.


# Część główna
# cusum, mosum, ME, RE, GEFP

stopa.model <- stopa_wig ~ stopa_sp500

#Score based CUSUM
cus = efp(stopa.model, type="Score-CUSUM")
plot(cus, functional=NULL)


Wg CUSUM zmieniła się wariancja modelu.


#Score based MOSUM
mos = efp(stopa.model, type="Score-MOSUM")
plot(mos)
plot(mos, functional=NULL)

MOSUM nie wskazuje na zmiany.

#Moving estimate
me <- efp(stopa.model, type="ME")
plot(me, functional = NULL)



ME sugeruje, że korelacja z S&P500 zmieniła się na chwilę. Wykres agregujący pozwoli pokazać ten moment na WIG. W tym miejscu muszę przyznać, że zrobiłem "błąd", bo operowałem na pakiecie xts, sądząc, że będzie łatwiej przy konstrukcji danych i wykresach. Jednakże pakiet strucchange działa (obecnie) formalnie na obiektach ts i jeśli jest to xts, to nie widać dat na wykresach. Drugim problemem jest odpowiednie dopasowanie osi x do wykresu. Aby pokazać każdy rok, trzeba użyć dodatkowego kodu, który objaśniłem na stronie o parametrach graficznych w R.

par(mfrow=c(2,1), mar=c(1,5,2,3), oma=c(2,0,0,0))
plot(me, functional="max")
plot(x=daty, y=as.ts(as.zoo(wig)), type="l", ylab="WIG", xaxt="n")
datySkr = seq(from=daty[1], to=daty[length(daty)], length.out=round(length(daty)/52))
axis(side=1, cex.axis=1, cex.lab=1, at=datySkr, labels=format(datySkr,"%Y"), las=2)




Z wykresów wynika, że tygodniowa kowariancja z amerykańskim indeksem wzrosła w latach 2005-2006. Obecnie wpływ USA na WIG wynosi niecałe 60% zgodnie z modelem regresji  i nic nie wskazuje, by miało się to zmienić w najbliższym czasie.

Możemy też wnioskować, że słaba kondycja naszej giełdy na tle USA wynikała nie ze spadku korelacji, ale średniej (niewarunkowej) stopy zwrotu. To znaczy średnia tygodniowa pozostała stabilna - jest praktycznie zerowa - i znajduje się w obszarze normalności, ale "losowo" spadła poniżej zera (Intercept < 0). Dzieje się to od 2008 r. Ostatnie tygodnie to jej poprawa i jeżeli iść "logiką" regresji do średniej to powinniśmy oczekiwać wychylenia na plus.

P. S. Pozostałych dwóch testów: RE i GEFP nie pokazuję już, bo wyniki są takie same jak przy CUSUM.

poniedziałek, 8 maja 2023

Czy warto kupować akcje deweloperów?

Program PISu 2% od kredytów na mieszkania wchodzi już w życie i WIG-NIERUCH ciągle rośnie bez zatrzymania:


Pytanie na ile opłacalne jest wchodzenie w ten rynek. Spekulacyjnie obecnie może i ma to sens, ale z drugiej strony rynek już zdyskontował wzrost popytu na mieszkania. Można najwyżej grać pod wygraną PO, która chce przebić PIS z ofertą 0%. 

Krótki termin jednak bywa zwodniczy, a inwestując w długim trzeba zastanowić się czy akurat ten sektor jest opłacalny.

Przede wszystkim zastanówmy się po co w ogóle deweloperzy wchodzą na giełdę akcji? Oczywiście mogą otrzymać niskim kosztem kapitał, ale inwestor, który może przyjąć rolę zarówno akcjonariusza jak i obligatariusza, powinien przeanalizować sprawę: portfel dzieli na mniej ryzykowne obligacje i ryzykowne akcje. Na obligacjach będzie dostawał dość pewny dochód z odsetek, a z akcji niepewne dywidendy. Nawet jeśli spółka płaci dywidendy, może je obniżyć. Akcjonariusz jednak nie traktuje dywidend jako czegoś istotnego, jeśli spółka ma coraz mniejsze przychody. W przypadku obligacji jest na odwrót - obligatariusza niewiele obchodzi sytuacja firmy (gdy już w nią zainwestował), bo ważne jest dla niego czy płacą odsetki w terminie. Wynika to wprost z asymetrycznej pozycji akcjonariuszy i wierzycieli, bo zobowiązania trzeba spłacać na początku. 

Już w tym miejscu rodzą się wątpliwości czy deweloperzy powinni wchodzić na rynek akcji. Projekty inwestycyjne w tej branży mają powtarzalny wzór, są schematyczne i przewidywalne. Trwają max 5 lat - pierwsze 2 lata to opracowanie koncepcji, zakup gruntu, projekt budowlany i wykonawczy, a potem rozpoczęcie budowy razem ze sprzedażą mieszkań - to trwa 2-3 lata i ostatni rok, dwa - sprzedaż.  Plany, nawet jeśli się zmieniają, to są precyzyjnie określone od początku do końca. Wiadomo, kiedy powinny generować zyski. Chociaż niektóre projekty są większe, inne mniejsze, to nie różnią się wiele poziomem ryzyka. Jeżeli deweloper przeprowadza wiele inwestycji jednocześnie, tak że jeden kończy, drugi zaczyna, to może generować stabilne przepływy.

Tak więc rynek obligacji i kredytów wydaje się najbardziej naturalnym dla tego sektora. Nie mamy tu dalekosiężnych wizji wdrażania nowych pomysłów. Od deweloperów nie oczekuje się specjalnej innowacyjności, tylko profesjonalizmu i rzetelności. Rynek akcji nie jest dla nich stworzony.

Z tego powodu inwestor musi się zastanowić, co pchało spółkę na ten rynek. Racjonalnie zarządzający majątkiem firmy powinni minimalizować koszty / wydatki. Tymczasem dywidendę płacą od zysku netto, a mogliby płacić ją od zysku operacyjnego, tzn. w formie odsetek. Pomniejszyliby w ten sposób podatki, ponieważ pomniejszony zysk operacyjny obniżyłby zysk brutto, a więc podatek dochodowy byłby niższy. Inwestor przekształciłby się z akcjonariusza w obligatariusza i nic by się dla niego nie zmieniło. Firma jednak miałaby dodatkowe poduszki finansowe. To się nazywa tarcza podatkowa.

Racjonalny zarząd powinien skorzystać z tej tarczy. Oczywiście nie tylko deweloper, ale szczególnie dotyczy to takich właśnie branż, w których projekty inwestycyjne są przewidywalne, schematyczne i trwają parę lat. 

No dobrze, ale ktoś zapyta co w sytuacji, gdy powstaje nowy deweloper, która potrzebuje środków, a nie ma jeszcze pieniędzy, by spłacać odsetki od pożyczek? Są obligacje zerokuponowe, które można wyemitować z terminem zapadalności na kilka lat i dać odpowiednio wysoką stopę zwrotu. 

Powiedzmy jednak, że firma już działa wiele lat na rynku mieszkaniowym, ale chce rozszerzyć działalność o rynek komercyjny (np. budować ośrodki turystyczne albo budynki dla innych firm), dlatego też chce wyemitować akcje, bo jest to na tyle niepewny biznes, że określenie terminu zapadalności obligacji samo w sobie rodziłoby dużą niepewność. No i to jest ciekawy przykład, bo rzeczywiście dopiero tutaj inwestycje w deweloperów stają się sensowne. Deweloper emituje więc akcje znacznie poniżej wartości księgowej kapitału własnego. Następnie mija 5-10 lat i firmie udało się przetrwać - ryzykowne projekty zostały pomyślnie zrealizowane, firma wybudowała i sprzedała nieruchomości. Teraz ma pieniądze, żeby zwrócić akcjonariuszom, ale może też je przeznaczyć na dalsze inwestycje. Ale zauważmy, że teraz sytuacja się zmieniła: wszyscy zobaczyli, że poradzili sobie na rynku nieruchomości komercyjnych, więc zarówno banki, jak i obligatariusze będą bardziej chętni udzielić im kredytów i pożyczek. Natomiast akcjonariusze na odwrót: skoro ryzyko specyficzne (czyli zależne od indywidualnych cech spółki) spadło, to znaczy, że firma nie musi już korzystać z kapitału akcyjnego.  Zatem spółka powinna nie tyle zacząć płacić dywidendy, co raczej wycofywać akcje z giełdy. Czyli powinna zacząć systematycznie nabywać akcje własne w celu umorzenia. Akcjonariusze zyskają na tej operacji, ponieważ kapitał własny na akcję wzrośnie. W ten sposób spółka wykupi i umorzy wszystkie akcje, wycofując się z giełdy. Jest to posunięcie racjonalne, bo przecież zarobione pieniądze zwraca ich właścicielom, a sama może teraz skorzystać z ofert banków i obligatariuszy.

Z powyższego wynika, że jeśli już mamy deweloperów na giełdzie, to nie powinni oni płacić dywidend, a jedynie nabywać własne akcje i umarzać. Wiele jednak z nich płaci dywidendy i to spore. Wynikałoby z tego, że ich giełdowi akcjonariusze są nieracjonalni, chyba że są krótkoterminowymi spekulantami.

Teoria teorią, ale jak to wygląda w praktyce? Zrobię krótką analizę opłacalności tego sektora. 

(1) Deweloperzy i fundusze nieruchomości na świecie vs. indeks światowy

MSCI, który zbiera dane finansowe z całego świata i prowadzi m.in. indeks MSCI World  , tj. indeks światowy, jest uznawany za dobry substytut portfela rynkowego w sensie CAPM. MSCI World Real Estate Index to pod-indeks tego pierwszego. Skupia wszystkie największe spółki sektora nieruchomości (działające na tym rynku oraz REITy - zob. tu).

Ostatnio MSCI porównał MSCI World vz MSCI World Real Estate Index (dla lat kwiecień 2008-kwiecień 2023):

 


 Po 15 latach indeks światowy daje stopę zwrotu wyższą o ok. 60%. 

Sceptyk zauważyłby, że rynek nieruchomości jest mniej ryzykowny niż np. rynek gier komputerowych i nic dziwnego, że ogólny indeks daje wyższe zwroty (wymagana stopa zwrotu jest wyższa). Jak najbardziej jest to słuszne spostrzeżenie, które wielu analitykom uchodzi uwadze. Dlatego oprócz stóp zwrotu, spojrzymy na wskaźnik Sharpe'a, który również został podany w tej samej analizie pod spodem:



Indeks światowy wygrywa na każdym polu: ma wyższą średnioroczną stopę zwrotu, niższe odchylenie standardowe, a więc wyższy wskaźnik Sharpe'a. Szczególnie warto spojrzeć na ostatnią kolumnę, którą oznaczono: od 30 grudnia 1994 r. Tak długi okres uwzględnia kilka cykli, więc może stać się najbardziej obiektywny. Podano tu zaanualizowane wyniki: 

Średnia stopa zwrotu

MSCI World Real Estate: 6,12%
MSCI World: średnia stopa zwrotu 7,91%

Wskaźnik Sharpe'a

MSCI World Real Estate: 0,28%
MSCI World: średnia stopa zwrotu 0,41%


Standardowo uznaje się, że średnioroczny zwrot na nieruchomościach powinien wynosić 6-7% i te statystyki udowadniają, że tak właśnie jest. Zauważmy jednak, że ostatnie 10 lat przyniosły zaledwie 3,6% w porównaniu z 9,3% na szerokim indeksie. Jeśli ktoś chce podjąć ryzyko, to może zagrać na prawo regresji do średniej, czyli pod wzrost indeksu nieruchomości.

Dalej, na załączonym obrazku są też podane podstawowe wskaźniki. Stopa dywidendy deweloperów jest znacznie powyżej średniej - potwierdza to tezę o ich nieracjonalności inwestowania na giełdzie akcji. To znaczy racjonalny inwestor wie, że jest to nieracjonalne, więc obniża swoją wycenę sprowadzając ją do... racjonalnych poziomów. Oczywiście wydaje się to paradoksem, ale pamiętajmy, że jeśli popyt jest niższy od podaży, to ceny spadają, a jeśli ten stan będzie utrzymywany, to ceny będą jedynie trzymane na takim poziomie, aby nie spadły zupełnie do irrracjonalnych poziomów, takich że deweloper mógłby sam wykupić swoje akcje i jeszcze zarobić, sprzedając swoje aktywa. Można by zapytać, czy przy nieco wyższych, ale nadal niskich cenach (poniżej kapitału własnego) deweloper nie mógłby tego samego zrobić? Pomijając kwestie, o których pisałem w art. Oczyszczanie zysku netto z niepewnych pozycji , trzeba pamiętać, że przy wycenie szacowanie kosztu kapitału zawiera aspekt psychologiczny z teorii gier (zob. ten wpis). Ze względu na frykcyjność przepływu kapitału racjonalny inwestor oczekiwałby informacji, że deweloper lub przynajmniej osoby z nim związane nabyłyby trochę akcji, sygnalizując rynkowi, że akcje są niedowartościowane (por. art. Strategia spółki wobec rynku akcji). W zasadzie chodziłoby by nawet bardziej o pokazanie, że deweloper wykazuje się racjonalnością, o której wyżej pisałem. To co nazywamy zaniżaniem cen przez spekulantów lub niedowartościowaniem akcji, możemy traktować jako celowy zabieg służący sprawdzeniu, na ile można obniżyć akcje bez reakcji ze strony spółki. Oprócz formalnej informacji sygnałem może być także gwałtowny wzrost popytu - zmiana wolumenu w połączeniu ze wzrostem kursów; jeżeli akcje są dużo poniżej kapitału własnego, a tak jest np. obecnie na WIG-NRUCH i nie ma reakcji zarządów ani istotna zmiana popytu na ich akcje, to inwestor / spekulant czyta to negatywnie: albo będzie słaby wynik, może strata, albo zarząd jest nieracjonalny i nie kupuje swoich akcji. Jeżeli to drugie, to powstaje niepewność odnośnie w ogóle osób zarządzających spółką w kierunku podejmowania decyzji operacyjnych. Mogą oczywiście nie interesować się ruchami na giełdzie, a jedynie swoją działalnością, ale akcjonariusze dostają w ten sposób komunikat, że mogą dalej zaniżać akcje... A więc dalej testują sprawdzając "cierpliwość" zarządu. 

Ktoś powie, że przecież inwestorzy są niezależni od siebie i nie zmawiają się, aby sprawdzać zarząd; czyli jacyś inni inwestorzy zauważą niską wycenę rynkową i sami zaczną to dyskontować. Tak rzeczywiście by było, gdyby inwestorów było nieskończenie wiele albo nie mieliby ograniczeń finansowych. Klasyczne teorie ekonomii i finansów tak właśnie zakładają, stąd właśnie jest ta teoria efektywności rynku. Jeśli uświadomimy sobie, że założenia  te nie są spełnione, to i taki efektywny rynek nie może być w pełni spełniony. I o ile na płynnych walorach te założenia jeszcze mogą być jako tako spełnione, to na niepłynnych nie będą i efektywność rynku sensu stricte nie może być z konieczności spełniona. To znaczy nawet pozytywna informacja może być przez rynek ignorowana, albo po chwilowym wystrzale, zapomniana, ponieważ rynek będzie chciał sprawdzić na ile może sobie pozwolić obniżać cenę. Z tego powodu obserwuje się dodatnią autokorelację ujemnych stóp zwrotu na małych i średnich spółkach - powstaje trend zniżkujący. 

Ktoś jeszcze zapyta, a czy to samo nie powinno występować w przypadku rosnących akcji? Nie, dlatego, że gdyby chodziło o testowanie zarządu w drugą stronę, to zarząd czy insiderzy, musieliby sprzedawać akcje, a więc zakładamy, że mają ich dużo. A przecież taka gra już jest uznawana za nieuczciwą, bo nie tego oczekuje się od spółki, by grała swoimi akcjami. I trudno byłoby nazwać taki zarząd racjonalnym, który zajmuje się zarabianiem na swoich akcjach. Spekulanci nie będą więc podnosić akcji w ramach trendu zwyżkującego, ponieważ nie oczekują reakcji zarządu, że akcje są za drogie.


(2) WIG-Nieruchomości vs. WIG

Przeprowadziłem też analizę samego WIG-NRUCH na tle WIG od 2006 do 2022:


Różnicy w stopach zwrotu już nie podaję, bo sami widzimy na wykresie. Obliczyłem też Uogólniony współczynnik Sharpe'a (GSR, zob. ten art.) przy założeniu dzisiejszej maksymalnej rentowności obligacji skarbowych, tj. 7,5%, i uzyskałem 

WIG Nieruchomości: 0,105
WIG: 0,13

Podobna zresztą różnica wyszła dla klasycznego SR (10,3 vs. 12,5%). 
To jest analiza na bieżącą chwilę, ale gdyby ocenić opłacalność tylko w badanym okresie, to należałoby ująć średnią stopę wolną od ryzyka z tego okresu. I tak 10-latki mają od 2002 r. rentowność na poziomie ok. 5% (zob. źródło danych): średnia 4,9, a mediana 5,2%. Po wstawieniu 5% do wzoru na GSR dostałem:

WIG Nieruchomości: 0,175
WIG: 0,22

Klasyczny SR wskazał podobnie (0,17 vs. 0,215). 

Widzimy, że jakby nie patrzeć, to inwestowanie w deweloperów nie ma sensu. Co innego oczywiście krótkoterminowa spekulacja, tak jak teraz.