from datetime import datetime
print(f'Päivitetty {datetime.now().date()} / Aki Taanila')
Päivitetty 2023-12-15 / Aki Taanila
Seuraavassa tarvitaan yfinance -kirjastoa, joka ei kuulu Anacondan vakioasennukseen. Voit asentaa sen komentoriviltä (Windows: Anaconda Prompt, macOS: Terminal/Pääte) komennolla conda install -c conda-forge yfinance
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
# Tätä tarvitaan datan noutamiseen Yahoo Finance -palvelusta
import yfinance as yf
# Tyyli vaikuttaa grafiikan ulkoasuun
sns.set_style('darkgrid')
Yahoo Finance -palvelu https://finance.yahoo.com/ sisältää tietoa osakkeista, valuutoista, raaka-aineista jne. Jos esimerkiksi haen palvelun hakutoiminnolla elisa, niin minulle selviää, että Elisan tunnus on ELISA.HE. Vastaavasti Telian tunnukseksi löydän TELIA1.HE. Seuraavassa haen Elisan ja Telian osakkeiden historiatietoja tähän päivään saakka.
Joka kerta kun suoritan koodin, saan mukaan myös tuoreimmat tiedot.
elisa = yf.download('ELISA.HE', start='2018-1-1')
telia = yf.download('TELIA1.HE', start='2018-1-1')
[*********************100%%**********************] 1 of 1 completed [*********************100%%**********************] 1 of 1 completed
# Datan alku- ja loppuosa
elisa
Open | High | Low | Close | Adj Close | Volume | |
---|---|---|---|---|---|---|
Date | ||||||
2018-01-02 | 32.970001 | 33.070000 | 32.689999 | 32.860001 | 25.996548 | 357134 |
2018-01-03 | 32.840000 | 33.070000 | 32.599998 | 32.689999 | 25.862055 | 348571 |
2018-01-04 | 32.770000 | 32.820000 | 32.660000 | 32.750000 | 25.909523 | 430650 |
2018-01-05 | 32.750000 | 32.970001 | 32.680000 | 32.910000 | 26.036100 | 443343 |
2018-01-08 | 32.930000 | 33.320000 | 32.930000 | 33.060001 | 26.154774 | 383662 |
... | ... | ... | ... | ... | ... | ... |
2023-12-11 | 41.970001 | 41.970001 | 41.459999 | 41.459999 | 41.459999 | 378711 |
2023-12-12 | 41.400002 | 41.840000 | 41.290001 | 41.540001 | 41.540001 | 171456 |
2023-12-13 | 41.500000 | 41.500000 | 40.840000 | 40.849998 | 40.849998 | 241663 |
2023-12-14 | 41.020000 | 41.639999 | 40.799999 | 40.799999 | 40.799999 | 431885 |
2023-12-15 | 40.930000 | 41.209999 | 40.759998 | 40.790001 | 40.790001 | 90321 |
1500 rows × 6 columns
# Datan alku- ja loppuosa
telia
Open | High | Low | Close | Adj Close | Volume | |
---|---|---|---|---|---|---|
Date | ||||||
2018-01-02 | 3.750 | 3.752 | 3.718 | 3.729 | 0.044918 | 1717521 |
2018-01-03 | 3.758 | 3.758 | 3.730 | 3.755 | 0.045232 | 1823437 |
2018-01-04 | 3.760 | 3.786 | 3.756 | 3.780 | 0.045533 | 1540541 |
2018-01-05 | 3.780 | 3.850 | 3.777 | 3.850 | 0.046376 | 1306020 |
2018-01-08 | 3.850 | 3.860 | 3.824 | 3.845 | 0.046316 | 2151101 |
... | ... | ... | ... | ... | ... | ... |
2023-12-11 | 2.322 | 2.328 | 2.305 | 2.320 | 2.320000 | 732361 |
2023-12-12 | 2.320 | 2.332 | 2.306 | 2.306 | 2.306000 | 934143 |
2023-12-13 | 2.306 | 2.311 | 2.280 | 2.284 | 2.284000 | 965178 |
2023-12-14 | 2.292 | 2.368 | 2.292 | 2.332 | 2.332000 | 1346397 |
2023-12-15 | 2.332 | 2.338 | 2.305 | 2.308 | 2.308000 | 517207 |
1500 rows × 6 columns
# Kehitys koko aikavälillä
fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(10, 3))
elisa['Close'].plot(ax=axs[0])
telia['Close'].plot(ax=axs[1])
<Axes: xlabel='Date'>
# Kehitys kuluvana vuonna
fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(10, 3))
elisa['Close']['2023':].plot(ax=axs[0])
telia['Close']['2023':].plot(ax=axs[1])
<Axes: xlabel='Date'>
resample-funktio aggregoi aikasarjan esimerkiksi päivätasolta kuukausitasolle.
resample-funktion parametrina käytettäviä arvoja:
https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases
# Päivän päätöshintojen aggregointi kuukausitasolle keskiarvoja käyttäen
fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(10, 3))
elisa['Close'].resample('M').mean().plot(ax=axs[0])
telia['Close'].resample('M').mean().plot(ax=axs[1])
<Axes: xlabel='Date'>
# Päivän päätöshintojen aggregointi vuosineljännestasolle keskiarvoja käyttäen
fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(10, 3))
elisa['Close'].resample('Q').mean().plot(ax=axs[0])
telia['Close'].resample('Q').mean().plot(ax=axs[1])
<Axes: xlabel='Date'>
# Osakkeiden vaihdon määrät (kpl) vuosineljänneksittäin (aggregointi summaa käyttäen)
# Viimeisen vuosineljänneksen kohdalla voi olla äkillinen pudotus, jos vuosineljännes on vasta aluillaan
fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(10, 3))
(elisa['Volume']/1000000).resample('Q').sum().plot(ax=axs[0])
(telia['Volume']/1000000).resample('Q').sum().plot(ax=axs[1])
axs[0].set_ylabel('Miljoonaa kpl')
Text(0, 0.5, 'Miljoonaa kpl')
Liukuvilla keskiarvoilla tasoitetaan satunnaisia piikkejä. Liukuvien tunnuslukujen laskenta onnistuu rolling-funktiolla.
Teknisessä analyysissä aikasarjan ja liukuvien keskiarvojen leikkauskohtia käytetään osto- ja myyntisignaaleina. Lisätietoa https://www.investopedia.com/articles/active-trading/052014/how-use-moving-average-buy-stocks.asp
# Elisan päätöshinnat
elisa['Close'].plot(figsize=(10, 6))
# Elisan päätöshintojen 50 päivän liukuvat keskiarvot
elisa['Close'].rolling(50).mean().plot()
# Elisan päätöshintojen 200 päivän liukuvat keskiarvot
elisa['Close'].rolling(200).mean().plot()
<Axes: xlabel='Date'>
# Elisan päätöshinnat
telia['Close'].plot(figsize=(10, 6))
# Elisan päätöshintojen 50 päivän liukuvat keskiarvot
telia['Close'].rolling(50).mean().plot()
# Elisan päätöshintojen 200 päivän liukuvat keskiarvot
telia['Close'].rolling(200).mean().plot()
<Axes: xlabel='Date'>
Muutosprosentit lasketaan pct_change-funktiolla. Tulos on desimaalimuodossa; tarvittaessa saan prosenttiluvut kertomalla luvulla 100.
# Hinnan muutokset prosentteina edellisestä päivästä
elisa['Elisa%'] = elisa['Close'].pct_change()
telia['Telia%'] = telia['Close'].pct_change()
# Tarkistetaan laskennan onnistuminen
elisa
Open | High | Low | Close | Adj Close | Volume | Elisa% | |
---|---|---|---|---|---|---|---|
Date | |||||||
2018-01-02 | 32.970001 | 33.070000 | 32.689999 | 32.860001 | 25.996548 | 357134 | NaN |
2018-01-03 | 32.840000 | 33.070000 | 32.599998 | 32.689999 | 25.862055 | 348571 | -0.005174 |
2018-01-04 | 32.770000 | 32.820000 | 32.660000 | 32.750000 | 25.909523 | 430650 | 0.001835 |
2018-01-05 | 32.750000 | 32.970001 | 32.680000 | 32.910000 | 26.036100 | 443343 | 0.004885 |
2018-01-08 | 32.930000 | 33.320000 | 32.930000 | 33.060001 | 26.154774 | 383662 | 0.004558 |
... | ... | ... | ... | ... | ... | ... | ... |
2023-12-11 | 41.970001 | 41.970001 | 41.459999 | 41.459999 | 41.459999 | 378711 | -0.012152 |
2023-12-12 | 41.400002 | 41.840000 | 41.290001 | 41.540001 | 41.540001 | 171456 | 0.001930 |
2023-12-13 | 41.500000 | 41.500000 | 40.840000 | 40.849998 | 40.849998 | 241663 | -0.016611 |
2023-12-14 | 41.020000 | 41.639999 | 40.799999 | 40.799999 | 40.799999 | 431885 | -0.001224 |
2023-12-15 | 40.930000 | 41.209999 | 40.759998 | 40.790001 | 40.790001 | 90321 | -0.000245 |
1500 rows × 7 columns
# Muodostan Elisan ja Telian muutosprosenteista uuden datan
muutokset = pd.concat([elisa['Elisa%'], telia['Telia%']], axis=1)
# Jos päätöshinnoissa on puuttuvia arvoja (kauppaa ei ole käyty), niin muutosprosenteissakin on puuttuvia arvoja
# Ne kannattaa korvata muutosprosentilla 0 käyttäen fillna-toimintoa
muutokset = muutokset.fillna(0)
# Tarkistetaan lopputulos
muutokset
Elisa% | Telia% | |
---|---|---|
Date | ||
2018-01-02 | 0.000000 | 0.000000 |
2018-01-03 | -0.005174 | 0.006972 |
2018-01-04 | 0.001835 | 0.006658 |
2018-01-05 | 0.004885 | 0.018519 |
2018-01-08 | 0.004558 | -0.001299 |
... | ... | ... |
2023-12-11 | -0.012152 | 0.001295 |
2023-12-12 | 0.001930 | -0.006034 |
2023-12-13 | -0.016611 | -0.009540 |
2023-12-14 | -0.001224 | 0.021016 |
2023-12-15 | -0.000245 | -0.010292 |
1500 rows × 2 columns
# Elisan ja Telian päivittäiset muutosprosentit kuluvana vuonna
(muutokset['2023':]*100).plot(figsize=(10, 6))
plt.ylabel('Muutosprosentti')
# Vaakaviiva nollan kohdalle; muutosprosentit vaihtelevat nollan molemmin puolin
plt.axhline()
<matplotlib.lines.Line2D at 0x29fba2c79d0>
# Tilastollisia tunnuslukuja muutosprosenteille
(muutokset*100).describe().round(2)
Elisa% | Telia% | |
---|---|---|
count | 1500.00 | 1500.00 |
mean | 0.02 | -0.02 |
std | 1.35 | 1.46 |
min | -9.22 | -13.45 |
25% | -0.60 | -0.71 |
50% | 0.06 | 0.03 |
75% | 0.71 | 0.73 |
max | 16.40 | 11.00 |
# Tunnuslukujen vertailua graafisesti
sns.boxplot(data=muutokset)
<Axes: >
# Päivät, jolloin muutosprosentti on jommallakummalla osakkeella ollut suurempi kuin 6 %
muutokset[(abs(muutokset['Elisa%'])>0.06) | (abs(muutokset['Telia%'])>0.06)]
Elisa% | Telia% | |
---|---|---|
Date | ||
2018-04-20 | -0.001125 | 0.083107 |
2018-07-13 | -0.092226 | -0.009455 |
2018-10-18 | -0.074751 | -0.000496 |
2019-04-04 | -0.063350 | -0.007843 |
2019-10-17 | 0.068757 | -0.062849 |
2020-03-09 | -0.040785 | -0.064356 |
2020-03-12 | -0.084077 | -0.134499 |
2020-03-17 | 0.164016 | 0.109976 |
2020-03-18 | 0.055873 | -0.061963 |
2020-04-03 | -0.076739 | -0.066333 |
2020-10-16 | -0.061020 | 0.005671 |
2022-10-21 | -0.018935 | -0.128406 |
2023-10-19 | -0.067709 | 0.082260 |
# Elisan ja Telian muutosprosentit korreloivat positiivisesti
muutokset.corr()
Elisa% | Telia% | |
---|---|---|
Elisa% | 1.000000 | 0.435786 |
Telia% | 0.435786 | 1.000000 |
# Muutosprosenttien välinen positiivinen korrelaatio näkyy myös hajontakaaviossa
(muutokset*100).plot(kind='scatter', x='Elisa%', y='Telia%')
<Axes: xlabel='Elisa%', ylabel='Telia%'>
# Liukuva korrelaatio kertoo miten muutosprosentit korreloivat eri aikoina
muutokset['Elisa%'].rolling(100).corr(muutokset['Telia%']).plot()
<Axes: xlabel='Date'>
Volatiliteetti kuvaa osakkeeseen liittyvää riskiä.
Volatiliteetti voidaan laska päivittäisten muutosprosenttien keskihajontana ( std-funktiolla ) ja se skaaltaaan vuositasolle kertomalla vuoden kaupantekopäivien lukumäärän neliöjuurella (sama kuin korotus potenssiin 0.5). Vuoteen sisältyvien kaupantekopäivien lukumäärä vaihtelee vuodesta toiseen. Tässä käytetty lukumäärää 252.
Liukuva volatiliteetti kuvaa, miten volatiliteetti (riski) on muuttunut ajan kuluessa.
# 200 päivän liukuva volatiliteetti
plt.figure(figsize = (10, 6))
(muutokset['Elisa%'].rolling(252).std() * (252**0.5)).plot(label='Elisa', legend=True)
(muutokset['Telia%'].rolling(252).std() * (252**0.5)).plot(label='Telia', legend=True)
<Axes: xlabel='Date'>
Elisan ja Telian päätöshinnat ovat eri suuruusluokkaa. Jos haluan kuvata ne päällekkäin samaan kaavioon, niin voin käyttää kahden arvoakselin kaaviota.
Värejä https://matplotlib.org/stable/gallery/color/named_colors.html
# Kuvion koko
plt.figure(figsize=(10, 6))
# Viivakaavio Elisan päätöshinnoista
eli = elisa['Close'].plot(color='dodgerblue')
# Elisan nimi, väri ja fonttikoko
plt.ylabel('Elisa', color='dodgerblue', fontsize=14)
# Elisan arvoakselin skaalaus (luvut valitaan läheltä pienintä ja suurinta päätöshintaa)
plt.ylim(30, 60)
# Luon Telialle kaavion (tel), jolla on yhteinen x-akseli Elisan kaavion kanssa
tel = eli.twinx()
# Viivakaavio Telian päätöshinnoista
telia['Close'].plot(ax=tel, color='darkviolet')
# Telian nimi, väri ja fonttikoko
plt.ylabel('Telia', color='darkviolet', fontsize=14)
# Telian arvoakselin skaalaus
plt.ylim(2.3, 4.4)
plt.title('Elisan ja Telian osakkeiden hinnan kehitys')
Text(0.5, 1.0, 'Elisan ja Telian osakkeiden hinnan kehitys')
viikonpaivat = ['ma', 'ti', 'ke', 'to', 'pe']
muutokset['Weekday'] = muutokset.index.weekday
df1 = (muutokset*100).groupby('Weekday')['Elisa%'].describe()
df1.index = viikonpaivat
df1
count | mean | std | min | 25% | 50% | 75% | max | |
---|---|---|---|---|---|---|---|---|
ma | 298.0 | 0.159200 | 1.154716 | -4.078550 | -0.517451 | 0.180600 | 0.810661 | 5.780507 |
ti | 305.0 | 0.051092 | 1.479570 | -3.897637 | -0.619272 | 0.036698 | 0.664944 | 16.401613 |
ke | 305.0 | 0.096927 | 1.188471 | -3.867992 | -0.517352 | 0.038242 | 0.707665 | 5.965581 |
to | 300.0 | -0.141318 | 1.503481 | -8.407721 | -0.769202 | -0.037183 | 0.630856 | 6.875676 |
pe | 292.0 | -0.050880 | 1.395204 | -9.222597 | -0.530880 | 0.084000 | 0.707627 | 3.601623 |
# Testataan onko maanantain ja torstain välillä merkitsevää eroa
# Vertailtavien ryhmien muodostaminen
ma = muutokset['Elisa%'][muutokset['Weekday']==0]
to = muutokset['Elisa%'][muutokset['Weekday']==3]
# Kahden riippumattoman (ind) otoksen t-testi
from scipy.stats import ttest_ind
ttest_ind(ma, to, equal_var=False, nan_policy='omit')
TtestResult(statistic=2.742283641638676, pvalue=0.006296353277847222, df=560.5476082167892)
df2 = (muutokset*100).groupby('Weekday')['Telia%'].describe()
df2.index = viikonpaivat
df2
count | mean | std | min | 25% | 50% | 75% | max | |
---|---|---|---|---|---|---|---|---|
ma | 298.0 | 0.052127 | 1.344098 | -6.435643 | -0.651818 | 0.081946 | 0.829898 | 4.142419 |
ti | 305.0 | 0.063761 | 1.409849 | -4.492134 | -0.609016 | 0.025587 | 0.731316 | 10.997615 |
ke | 305.0 | -0.011633 | 1.380628 | -6.196316 | -0.743195 | -0.098121 | 0.740223 | 5.843288 |
to | 300.0 | -0.145500 | 1.604992 | -13.449943 | -0.771205 | 0.000000 | 0.592920 | 8.225973 |
pe | 292.0 | -0.066919 | 1.566218 | -12.840602 | -0.710870 | 0.058161 | 0.714696 | 8.310701 |
# Testataan onko tiistain ja torstain välillä merkitsevää eroa
# Vertailtavien ryhmien muodostaminen
ma = muutokset['Telia%'][muutokset['Weekday']==1]
to = muutokset['Telia%'][muutokset['Weekday']==3]
# Kahden riippumattoman (ind) otoksen t-testi
from scipy.stats import ttest_ind
ttest_ind(ma, to, equal_var=False, nan_policy='omit')
TtestResult(statistic=1.7027391806687329, pvalue=0.08914311406346705, df=590.5275985926206)
Data-analytiikka Pythonilla: https://tilastoapu.wordpress.com/python/