import statsmodels.formula.api as smf
import statsmodels.api as sm
import pandas as pd
= pd.read_csv('gld_uso.csv')
df = ['GLD','USO'] cols
Borsada ortalamaya dönüş (mean-reversion) ile nasıl işlem yapılır? Daha önce örnekleri gördük, Z-skoru yarattık ve ona ters yönde işlem yaptık. Altta bazı ek noktalar gösterilecek.
Lineer Regresyon ile bulunan yatırım bölüştürme oranı (hedge ratio) zaman serisinin her anı için “en iyi’’ olmayabilir. Bu durumda yatırımcı belli bir pencere üzerinden yakın tarihe bakıp oranı sürekli tekrar tekrar hesaplamayı seçebilir. Altta görülen kod bunu yapıyor,
import statsmodels.api as sm
=20;
lookback'hedgeRatio'] = np.nan
df[
for t in range(lookback,len(df)):
= np.array(df['GLD'])[t-lookback:t]
x = sm.add_constant(x)
x = np.array(df['USO'])[t-lookback:t]
y 'hedgeRatio'] = sm.OLS(y,x).fit().params[1]
df.loc[t,
= np.ones(df[cols].shape); yport[:,0] = -df['hedgeRatio']
yport = yport.sum(axis=1)
yport = pd.Series(yport)
yport = yport.rolling(window=20).mean()
data_mean = yport.rolling(window=20).std()
data_std 'numUnits'] = -1*(yport-data_mean) / data_std
df[= np.ones(df[cols].shape) * np.array([df['numUnits']]).T
tmp1 = np.ones(df[cols].shape); tmp2[:, 0] = -df['hedgeRatio']
tmp2 = pd.DataFrame(tmp1 * tmp2 * df[cols])
positions = positions.shift(1) * (df[cols] - df[cols].shift(1)) / df[cols].shift(1)
pnl = pnl.sum(axis=1)
pnl =pnl / np.sum(np.abs(positions.shift(1)),axis=1)
retprint ('APR', ((np.prod(1.+ret))**(252./len(ret)))-1)
print ('Sharpe', np.sqrt(252.)*np.mean(ret)/np.std(ret))
APR 0.23319087620701384
Sharpe 1.1215726543503066
Yıllık getiri yüzde 23 Sharpe oranı 1.12. Fena değil çünkü bu seri koentegre bile değil,
import sys; sys.path.append('../tser_030_coint')
import pyconometrics
print (pyconometrics.cadf(np.matrix(df['GLD']).H,
'USO']).H,0,1)) np.matrix(df[
{'alpha': np.float64(-0.0031124833978737783), 'adf':
np.float64(-1.5150247935770818), 'crit': matrix([[-3.88031, -3.35851,
-3.03798, -1.01144, -0.65334, 0.15312]]), 'nlag': 1, 'nvar': 1}
Oran Kullanımı
Eğer basit bir şekilde \(y/x\) ile iki varlığını oranını “işlem sinyali’’ olarak kullansaydık ne olurdu? Ayrıca diyelim ki her iki varlığa eşit para yatırıyoruz,
'hedgeRatio'] = df['USO'] / df['GLD']
df[= df['hedgeRatio'].rolling(window=20).mean()
data_mean = df['hedgeRatio'].rolling(window=20).std()
data_std 'numUnits'] = -1*(df['hedgeRatio']-data_mean) / data_std
df[= df[['numUnits','numUnits']].copy()
positions = positions * np.array([-1., 1.])
positions = positions.shift(1) * np.array((df[cols] - df[cols].shift(1))
pnl / df[cols].shift(1))
= pnl.fillna(0).sum(axis=1)
pnl =pnl / np.sum(np.abs(positions.shift(1)),axis=1)
retprint ('APR', ((np.prod(1.+ret))**(252./len(ret)))-1.)
print ('Sharpe', np.sqrt(252.)*np.mean(ret)/np.std(ret))
APR -0.14067355886298372
Sharpe -0.7495829329023052
1+ret)-1)
plt.plot(np.cumprod('tser_mrimp_01.png') plt.savefig(
Sonuç iyi değil.
Bollinger Bantları
Şimdiye kadar gösterilen lineer strateji basit: tek birimlik durağanlaştırılmış portföy eğer piyasanın yürüyen ortalama üzerinden olan fiyatın üzerine çıkmışsa, bu çıkış oranında varlık al, düşüşte satmaya başla. Bölüştürme oranı iki kere kullanılıyor yani, ilk önce yürüyen ortalama fiyatlarını birleştirmek için, ve sonra en son piyasa fiyatlarını birleştirmek için. Bu iki serinin birisi durağan serinin son hali, ortalamadan sapmayı bu ikinci serinin birincisine oranla ölçüyoruz.
Bu strateji seçildi çünkü hiçbir dış parametre gerektirmeyen bir strateji. Az parametre iyi bir şey, böylece aşırı uygunluk (overfitting) gibi problemlerden biraz daha uzaklaşmış oluyoruz (parametreler geçmiş veriye aşırı iyi uyuyor, bu sebeple geleceği tahmin yeteneği kayboluyor).
Bollinger bantları üstteki stratejinin bir uzantısı, yine ortalamadan
uzaklaşınca pozisyona giriyoruz, fakat bu uzaklaşmanın kaç standart
sapma oranında olduğuna bakıyoruz. Mesela uzaklaşma 1 standart sapma
oranından fazla ise girebiliriz, 0 standart sapma oranında ise (yani
ortalama üzerinde) pozisyondan çıkarız (bu parametre isimleri sırasıyla
entryZscore
, exitZscore
. Ya da
entryZscore=1
, exitZscore=-1
diyebilirdik, bu
durumda üstte ve altta 1 standart sapmadan fazla olduğu zaman alım,
satım olurdu.
=20;
lookback
'hedgeRatio'] = np.nan
df[for t in range(lookback,len(df)):
= np.array(df['GLD'])[t-lookback:t]
x = sm.add_constant(x)
x = np.array(df['USO'])[t-lookback:t]
y 'hedgeRatio'] = sm.OLS(y,x).fit().params[1] df.loc[t,
= ['GLD','USO']
cols
= np.ones(df[cols].shape)
yport 0] = -df['hedgeRatio']
yport[:,= yport * df[cols]
yport = yport[cols].sum(axis=1)
yport = pd.Series(yport)
yport
= yport.rolling(window=20).mean()
data_mean = yport.rolling(window=20).std()
data_std =(yport-data_mean)/data_std
zScore
=1.
entryZscore=0
exitZscore
=zScore < -entryZscore
longsEntry=zScore > -exitZscore
longsExit=zScore > entryZscore
shortsEntry=zScore < exitZscore
shortsExit
= pd.Series([np.nan for i in range(len(df))])
numUnitsLong = pd.Series([np.nan for i in range(len(df))])
numUnitsShort 0] = 0.
numUnitsLong[0] = 0.
numUnitsShort[
= 1.0
numUnitsLong[longsEntry] = 0.0
numUnitsLong[longsExit] = numUnitsLong.fillna(method='ffill')
numUnitsLong
= -1.0
numUnitsShort[shortsEntry] = 0.0
numUnitsShort[shortsExit] = numUnitsShort.fillna(method='ffill')
numUnitsShort 'numUnits'] = numUnitsShort + numUnitsLong
df[
= np.ones(df[cols].shape) * np.array([df['numUnits']]).T
tmp1 = np.ones(df[cols].shape); tmp2[:, 0] = -df['hedgeRatio']
tmp2 = pd.DataFrame(tmp1 * tmp2 * df[cols])
positions = positions.shift(1) * (df[cols] - df[cols].shift(1)) / df[cols].shift(1)
pnl = pnl.sum(axis=1)
pnl =pnl / np.sum(np.abs(positions.shift(1)),axis=1)
ret=ret.fillna(0)
retprint ('APR', ((np.prod(1.+ret))**(252./len(ret)))-1)
print ('Sharpe', np.sqrt(252.)*np.mean(ret)/np.std(ret))
APR 0.1977158548013851
Sharpe 1.064087612417886
1+ret)-1)
plt.plot(np.cumprod(u'Kümülatif Birleşik Getiri')
plt.title('tser_mrimp_02.png') plt.savefig(
Kalman Filtreleri ile Dinamik Lineer Regresyon
Gerçekten koentegre halinde olan iki fiyat zaman serisi için yapılacaklar basit - bulabildiğin kadar tarihi veri bul, basit lineer regresyon ya da Johansen test kullanarak özvektörleri bul. Fakat diğer yazılarda gördüğümüz gibi pür koentegresyon çok az sayıda fiyat zaman serisinin erişebildiği bir mertebe. O zaman, zamana göre değişebilecek yatırım bölüştürme oranını (hedge ratio) nasıl hesaplayacağız? Diğer örneklerde gördük, bir geriye bakış penceresi kararlaştır, ve oranı sadece bu pencere içindeki tarihe veriden hesapla. Bu yaklaşımın dezavantajı, eğer pencere ufak ise pencere kaydırıldıkça yatırım oranı aşırı sapmalar gösterebilmesi. Aynı durum ortalamayı ve standard sapma için yürüyen ortalama ve yürüyen sapma kullanırken de ortaya çıkacak. O zaman eğer en sondaki verilere öncekilerden daha fazla ağırlık veren, ve kullanıcının kafasından attığı bir başlangıç noktasına göre pencere oluşturmayan bir yöntem olsa bu bir ilerleme olurdu. Yatırım bölüştürme oranını Kalman filtresi (KF) kullanarak hesaplayarak bu ilerlemeyi sağlamayı umuyoruz [1, sf 74].
KF hakkında detaylar [2] yazısında bulunabilir; KF formüllerinin türetilmesi orada anlatıldı. Bu yazıda gereken bölüştürme oranı, ve yan ürün olarak bu oranın ortalamasını ve uçuculuğunu (volatility) hesaplamak, o zaman gizli değişken bölüştürme oranı \(\beta\), görünen (observable) değişken ise fiyat zaman serisi \(y\) olacak, yani daha önce basit lineer regresyona \(y\) olarak verilen fiyat serisi. Tüm KF modeli,
\[ \beta_t = I \cdot \beta_{t-1} + \omega_{t-1}\]
\[ y_t = x_t \beta_t + \epsilon_t \]
\(\omega_{t-1},\epsilon_t\) Gaussian gürültü olmak üzere.
Yukarıda ilginç birkaç “numara’’ yapıldı; aslında her iki seriyi de, hem \(x\)’i, hem \(y\)’yi biliyoruz, yani onlar”görünüyor’’. Ama yapmak istediğimiz numara bağlamında onlardan sadece birini görünen yaptık, ayrıca gizli değişkeni \(\beta\) yaptık, genellikle bu tür bir parametre gizli değişkeni transforme eden matris olarak ele alınırdı. Görünen tek veri ise modele göre \(y\). Bu durumda \(x\) gizli değişkeni transforme eden matris gibi kullanılıyor, bu da ilginç.
Üstteki formüllerden birincisi geçiş (transition) formülü, ve biz eldeki tüm verileri temsil eden tek bir \(\beta\) aradığımız için bu \(\beta\)’nin değişmediğini modele söylüyoruz, bu sebeple geçiş matrisi \(I\), yani birim matris, bu matris çarpımda hiçbir etki yaratmıyor.
import numpy as np
def kalman_filter(x,y):
=0.0001
delta=0.001
Ve
= np.ones(len(y))*np.nan
yhat = np.ones(len(y))*np.nan
e = np.ones(len(y))*np.nan
Q = np.zeros((2,2))
R = np.zeros((2,2))
P
= np.matrix(np.zeros((2,len(y)))*np.nan)
beta
=delta/(1-delta)*np.eye(2)
Vw
0]=0.
beta[:,
for t in range(len(y)):
if (t > 0):
=beta[:, t-1]
beta[:, t]=P+Vw
R
=np.dot(x[t, :],beta[:, t])
yhat[t]
= np.matrix(x[t, :])
xt = np.dot(np.dot(xt,R),xt.T) + Ve
Q[t]
=y[t]-yhat[t]
e[t]
=np.dot(R,np.matrix(x[t, :]).T) / Q[t]
K
=beta[:, t]+np.dot(K,np.matrix(e[t]))
beta[:, t]
=R-np.dot(np.dot(K,xt),R)
P
return beta, e, Q
import pandas as pd, kf
= pd.read_csv('../tser_030_coint/ETF.csv')
ewdf
= ewdf[['ewa']].copy()
x = ewdf[['ewc']].copy()
y 'intercept'] = 1.
x[
= np.array(x)
x = np.array(y)
y
= kf.kalman_filter(x,y) beta, e, Q
0, :].T)
plt.plot(beta['EWC(y) ve EWA(x) Arasındaki Eğim - Beta[0,t]')
plt.title('tser_mrimp_03.png') plt.savefig(
1, :].T)
plt.plot(beta['Kesi, Beta[1,t]')
plt.title('tser_mrimp_04.png') plt.savefig(
Bu modelin güzel yan etkilerinden biri şu oldu: KF’in doğal olarak hesapladığı parametreler ile direk bir ortalamaya-dönüş stratejisi kodlayabiliriz. \(e_t\) içinde ölçüm tahmin hatası var, ki bu hata EWC-EWA’nın tahmin edilen ortalamasından sapmasından başka bir şey değil. Bu sapmayı satın alırız, eğer çok pozitif ise al-tut yaparız, çok negatif ise açığa satış. Çok pozitif, çok negatif neye göre belirlenir? Bu da \(e_t\)’nin tahmin edilen standart sapmasından başka bir şey değil, ki bu bilgi de \(\sqrt{Q_t}\) içinde! Her iki parametreyi grafiklersek alttaki görüntü çıkıyor.
2:], 'r')
plt.plot(e[2:]))
plt.plot(np.sqrt(Q['tser_mrimp_05.png') plt.savefig(
Geri kalanlar daha önce Bollinger bantlarında gördüğümüz gibi.
= ['ewa','ewc']
cols = ewdf[cols]
y2
=e < -1*np.sqrt(Q)
longsEntry=e > -1*np.sqrt(Q)
longsExit
=e > np.sqrt(Q)
shortsEntry=e < np.sqrt(Q)
shortsExit
= pd.Series([np.nan for i in range(len(ewdf))])
numUnitsLong = pd.Series([np.nan for i in range(len(ewdf))])
numUnitsShort 0]=0.
numUnitsLong[0]=0.
numUnitsShort[
=1.
numUnitsLong[longsEntry]=0
numUnitsLong[longsExit]= numUnitsLong.fillna(method='ffill')
numUnitsLong
=-1.
numUnitsShort[shortsEntry]=0
numUnitsShort[shortsExit]= numUnitsShort.fillna(method='ffill')
numUnitsShort
'numUnits']=numUnitsLong+numUnitsShort
ewdf[
= np.tile(np.matrix(ewdf.numUnits).T, len(cols))
tmp1 = np.hstack((-1*beta[0, :].T,np.ones((len(ewdf),1))))
tmp2 = np.array(tmp1)*np.array(tmp2)*y2
positions = pd.DataFrame(positions)
positions
= np.tile(np.matrix(ewdf.numUnits).T, len(cols))
tmp1 = np.hstack((-1*beta[0, :].T,np.ones((len(ewdf),1))))
tmp2 = np.array(tmp1)*np.array(tmp2)*y2
positions
= pd.DataFrame(positions)
positions
= np.array(positions.shift(1))
tmp1 = np.array(y2-y2.shift(1))
tmp2 = np.array(y2.shift(1))
tmp3 = np.sum(tmp1 * tmp2 / tmp3,axis=1)
pnl = pnl / np.sum(np.abs(positions.shift(1)),axis=1)
ret = ret.fillna(0)
ret print ('APR', ((np.prod(1.+ret))**(252./len(ret)))-1)
print ('Sharpe', np.sqrt(252.)*np.mean(ret)/np.std(ret))
APR 0.262251943494046
Sharpe 2.361949085176113
ETF ve ETF’in Öğe Hisseleri Arasında Arbitraj
Bir ETF ve onu oluşturan öğe hisseler arasında da arbitraj fırsatları vardır. Bu portföyü oluşturmak için her öğe hisse ile ETF arasında teker teker koentegrasyon aranır, diğerleri atılır. Bu örneği dünyanın belki de en ünlü ETF’i üzerinde göstereceğiz. Standart & Poors endeksini baz alan SPY.
Tarihi veri olarak Ocak 1, 2007 ile Aralık 31, 2007 arasını seçtik, bu aralıktaki SPY öğelerinin SPY’in kendisi ile en az yüzde 90 koentegre olma şartını Johansen testi ile kontrol edeceğiz. Ardından bu seçilen senetlerin her birine eşit sermaye ayıracağız, ve tüm portföy üzerinde tekrar Johansen testi uygulayıp hala koentegre olup olmadığını kontrol edeceğiz. Bu ikinci test lazım çünkü her öğeye verilen kafamıza göre verdiğimiz (burada eşit) sermaye ağırlığı üzerinden oluşturulmuş portföyün illa koentegre olacağı gibi bir şart yoktur. Bu ikinci test için log fiyat kullanacağız, çünkü bu portföyü her gün tekrar dengeleyeceğimizi bekliyoruz, yani senet miktarı üzerinden değil sermaye seviyesini sabit tutacağız.
import pandas as pd, zipfile
with zipfile.ZipFile('SPY3.zip', 'r') as z:
= pd.read_csv(z.open('SPY3.csv'),sep=',')
dfspy3
= dfspy3.set_index('Date')
dfspy3 = dfspy3[(dfspy3.index>=20070101) & (dfspy3.index<=20071231)]
train = dfspy3[(dfspy3.index > 20071231)]
testspy3 = pd.DataFrame(index=dfspy3.columns)
resdf 'isCoint'] = np.nan
resdf[
import sys; sys.path.append('../tser_coint')
from johansen import coint_johansen, print_johan_stats
for s in dfspy3.columns:
if s == 'SPY': continue
# johansen cagrisini kullaniyoruz boylece y,x hangisi secmemiz
# gerekmiyor
= train[[s,'SPY']].dropna()
data if len(data) < 250: continue
= coint_johansen(data, 0, 1)
res if res.lr1[0] > res.cvt[0][0]:
'isCoint'] = True
resdf.loc[s,print (resdf.isCoint.sum())
98
98 tane senet koentegre imiş. Şimdi bu senetlerle portföy oluşturalım, ve tekrar koentegrasyon testi yapalım,
= list(resdf[resdf.isCoint==True].index)
coint_cols = train[coint_cols]
yN = np.log(yN).sum(axis=1)
logMktVal_long = pd.concat([logMktVal_long, np.log(train.SPY)],axis=1)
ytest = coint_johansen(ytest, 0, 1)
res print_johan_stats(res)
trace statistic [15.86864835 6.19735725]
critical vals %90,%95,%99
r<=0 [13.4294 15.4943 19.9349]
r<=1 [2.7055 3.8415 6.6349]
eigen statistic [9.6712911 6.19735725]
critical values %90,%95,%99
r<=0 [12.2971 14.2639 18.52 ]
r<=1 [2.7055 3.8415 6.6349]
ozdegerler [0.0380959 0.02458181]
ozvektorler
[[ 1.09386171 -0.27989806]
[-105.55999232 56.09328286]]
= np.ones((len(testspy3),resdf.isCoint.sum()))*res.evec[0,0]
tmp1 = np.ones((len(testspy3),1))*res.evec[1,0]
tmp2 = np.hstack((tmp1,tmp2))
weights = testspy3[coint_cols + ['SPY']]
yNplus = np.sum(weights * np.log(yNplus),axis=1)
logMktVal =5
lookback= logMktVal.rolling(window=lookback).mean()
data_mean = logMktVal.rolling(window=lookback).std()
data_std = -1*(logMktVal-data_mean) / data_std
numUnits
= np.reshape(numUnits, (len(numUnits),1))
numUnits2 = pd.DataFrame(np.tile(numUnits2, weights.shape[1]),\
positions =yNplus.columns)*weights
columns= np.log(yNplus)-np.log(yNplus.shift(1))
tmp1 = np.sum(np.array(positions.shift(1)) * np.array(tmp1), axis=1)
pnl = pnl / np.sum(np.abs(positions.shift(1)),axis=1)
ret print ('APR', ((np.prod(1.+ret))**(252./len(ret)))-1)
print ('Sharpe', np.sqrt(252.)*np.mean(ret)/np.std(ret))
APR 0.044929874512839474
Sharpe 1.323109852605057
Sonuç ilk deneme için fena sayılmaz; Bazı basit ilerlemeler mümkündür, mesela her zaman aralığı için portföyü oluşturan senetleri değiştirmek.
1+ret)-1)
plt.plot(np.cumprod('tser_mrimp_06.png') plt.savefig(
Trendli Ortalamaya Dönüş
Bir tane de benden. Finans zaman serilerinin çoğunlukla bir trend’e dönüş yaptığını görebiliriz. Eğer bu trend’i çıkartırsak geriye kalan nedir? Ortalamaya dönüş yapan, durağan bir zaman serisi değil mi? S&P 500 üzerinde görelim,
import pandas as pd
= pd.read_csv('../tser_010_back/SPY.csv',index_col='Date',parse_dates=True)
df 'SPY'] = df[['Adj Close']]
df[= df[df.index < '2005-01-01']
df
df.SPY.plot()'tser_mrimp_07.png') plt.savefig(
Belli noktalarda geriye dönerek belli bir pencere içindeki zaman seri parçası üzerinde lineer regresyon uyguluyoruz. Daha sonra bu uydurduğumuz çizgiyi ileri dönük tahmin olarak kullanıyoruz, bu tahminin altına düşüşlerde alım, yukarı çıkışlarda satım yapıyoruz.
import statsmodels.api as sm
= 100; forward = 30
lookback = range(lookback, len(df), forward)
forward_points
= np.ones((lookback,2))
x 1] = np.array(range(lookback))
x[:,
for t in forward_points:
= df.SPY[t-lookback:t]
y = sm.OLS(y,x).fit()
f 'intercept'] = f.params[0]
df.loc[df.index[t],'slope'] = f.params[1]
df.loc[df.index[t],
'ols'] = np.nan
df[= np.array(range(lookback))
x_lookback for t in forward_points:
= x_lookback * df.iloc[t].slope + df.iloc[t].intercept
y -lookback:t].loc[:,'ols'] = y
df.iloc[t'SPY','ols']].plot()
df[['tser_mrimp_08.png') plt.savefig(
Trend’i çıkartınca geriye kalanın hakikaten durağan olduğunu görelim,
'MR'] = df.SPY - df.ols
df[
df.MR.plot()'tser_mrimp_09.png') plt.savefig(
=5
win= df.MR.rolling(window=win).mean()
data_mean = df.MR.rolling(window=win).std()
data_std 'mktVal'] = -1*(df.MR-data_mean) / data_std
df[= df['mktVal'].shift(1) * (df['MR']-df['MR'].shift(1))/ df['MR'].shift(1)
pnl =pnl.fillna(0) / np.sum(np.abs(df['mktVal'].shift(1)))
retprint ('APR', ((np.prod(1.+ret))**(252./len(ret)))-1.)
print ('Sharpe', np.sqrt(252.)*np.mean(ret)/np.std(ret))
APR 0.2801817694094304
Sharpe 0.8884148597414459
Kaynaklar
[1] Chan, Algorithmic Trading
[2] Bayramlı, Fizik, Kalman Filtreleri