日本株の株価を取得してpandasデータフレームに格納する

# これはなに？
* pandas_datareaderというライブラリから海外株は取得できますが、日本株は取得できません。
* jsmというライブラリによってBeautifulSoupを利用してxmlパースで日本株のデータを取得できますが、pandas DataFrame形式にしてはくれません。


そこで両者をいいとこどりして、データのダウンロードと同時にpandas DataFrameに直してくれるコードを書きました。

# 使用例

## 株価の取得
```python:ファーストリテイリング(証券コード9983)の日足を現在から30日遡って取得
from read_nikkei import get_jstock
df = get_jstock(6502)
df.tail()
```
|   | Open  | High  | Low  | Close  | Adj Close  | Volume |
|:--|--:|--:|--:|--:|--:|--:|
| Date  |  |  |  |  |  |  |
| 2017-12-11  | 44800  | 45000  | 44410  | 44630  | 44630  | 562800 |
| 2017-12-12  | 44630  | 44770  | 44320  | 44340  | 44340  | 511200 |
| 2017-12-13  | 44480  | 44720  | 44080  | 44340  | 44340  | 531100 |
| 2017-12-14  | 44100  | 44400  | 43980  | 44050  | 44050  | 393500 |
| 2017-12-15  | 43920  | 45080  | 43840  | 44600  | 44600  | 646900 |


```python:任天堂(証券コード7974)の2016年の週足を取得
df = get_jstock(7974, 'W', start=pd.Timestamp('2016'), end=pd.Timestamp('2017'))
df.tail()
```

|   | Open  | High  | Low  | Close  | Adj Close  | Volume |
|:--|--:|--:|--:|--:|--:|--:|
| Date  |  |  |  |  |  |  |
| 2016-11-28  | 27810  | 28245  | 26860  | 27045  | 27045  | 8573500 |
| 2016-12-05  | 26850  | 29420  | 26650  | 29255  | 29255  | 10525400 |
| 2016-12-12  | 29900  | 30180  | 26030  | 26405  | 26405  | 16958500 |
| 2016-12-19  | 25000  | 25430  | 23505  | 23595  | 23595  | 15262800 |
| 2016-12-26  | 23860  | 24820  | 23800  | 24540  | 24540  | 8286600 |


```python:ソフトバンク(証券コード9984)の2017/2/1から遡って100日前からの日足を取得
df = get_jstock(9984, end=pd.Timestamp('20170201'), periods=100)
df.head(5).append(df.tail(5))
```


|   | Open  | High  | Low  | Close  | Adj Close  | Volume |
|:--|--:|--:|--:|--:|--:|--:|
| Date  |  |  |  |  |  |  |
| 2016-10-24  | 6584  | 6610  | 6515  | 6524  | 6524  | 3428700 |
| 2016-10-25  | 6517  | 6571  | 6515  | 6531  | 6531  | 4082600 |
| 2016-10-26  | 6532  | 6597  | 6504  | 6585  | 6585  | 3835600 |
| 2016-10-27  | 6568  | 6622  | 6526  | 6622  | 6622  | 3859800 |
| 2016-10-28  | 6620  | 6685  | 6607  | 6654  | 6654  | 12014500 |
| 2017-01-26  | 8772  | 8960  | 8765  | 8956  | 8956  | 12706000 |
| 2017-01-27  | 8983  | 9066  | 8920  | 8977  | 8977  | 10963300 |
| 2017-01-30  | 8900  | 8967  | 8833  | 8853  | 8853  | 6160500 |
| 2017-01-31  | 8703  | 8822  | 8688  | 8701  | 8701  | 7696500 |
| 2017-02-01  | 8650  | 8814  | 8636  | 8805  | 8805  | 6654700 |

```python:東芝(証券コード6502)の取得できる最も古い日から現在までの月足を取得
df = get_jstock(6502, 'M', start='first', end='last')
df.head(5).append(df.tail(5))
```

|   | Open  | High  | Low  | Close  | Adj Close  | Volume |
|:--|--:|--:|--:|--:|--:|--:|
| Date  |  |  |  |  |  |  |
| 2000-01-01  | 820.0  | 855.0  | 761.0  | 839.0  | 839.0  | 169788000 |
| 2000-02-01  | 835.0  | 971.0  | 806.0  | 897.0  | 897.0  | 245281000 |
| 2000-03-01  | 937.0  | 1070.0  | 853.0  | 1046.0  | 1046.0  | 254632000 |
| 2000-04-01  | 1046.0  | 1080.0  | 891.0  | 1048.0  | 1048.0  | 149523000 |
| 2000-05-01  | 1079.0  | 1171.0  | 967.0  | 1040.0  | 1040.0  | 186095000 |
| 2017-08-01  | 249.0  | 326.0  | 244.0  | 306.0  | 306.0  | 776378000 |
| 2017-09-01  | 302.0  | 338.0  | 295.0  | 315.0  | 315.0  | 570638000 |
| 2017-10-01  | 307.0  | 347.0  | 298.0  | 329.0  | 329.0  | 476897000 |
| 2017-11-01  | 330.0  | 332.0  | 273.0  | 276.0  | 276.0  | 1044946000 |
| 2017-12-01  | 279.0  | 329.0  | 273.0  | 323.0  | 323.0  | 1038915000 |


## 株価の保存
データの保存は「pandas DataFrame　保存」でググって下さい。
`df.to_csv('ファイル名.csv')`とか`df.to_pickle('ファイル名.pkl')`とか。



# `read_nikkei`ソースコード
```python:read_nikkei.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import pandas as pd
import numpy as np
from pandas.core import common as com
import jsm

def set_span(start=None, end=None, periods=None, freq='D'):
    """ 引数のstart, end, periodsに対して
    startとendの時間を返す。

    * start, end, periods合わせて2つの引数が指定されていなければエラー
    * start, endが指定されていたらそのまま返す
    * start, periodsが指定されていたら、endを計算する
    * end, periodsが指定されていたら、startを計算する
    """
    if com._count_not_none(start, end, periods) != 2:  # Like a pd.date_range Error
        raise ValueError('Must specify two of start, end, or periods')
    start = start if start else (pd.Period(end, freq) - periods).start_time
    end = end if end else (pd.Period(start, freq) + periods).start_time
    return start, end


def get_jstock(code, freq='D', start=None, end=None, periods=None):
    """get Japanese stock data using jsm
    Usage:
        `get_jstock(6502)`
        To get TOSHIBA daily from today back to 30days except holiday.

        `get_jstock(6502, 'W', start=pd.Timestamp('2016'), end=pd.Timestamp('2017'))`
        To get TOSHIBA weekly from 2016-01-01 to 2017-01-01.

        `get_jstock(6502, end=pd.Timestamp('20170201'), periods=50)`
        To get TOSHIBA daily from 2017-02-01 back to 50days except holiday.

        `get_jstock(6502, 'M', start='first', end='last')`
        To get TOSHIBA monthly from 2000-01-01 (the date of start recording) to today.
    """
    # Default args
    if com._count_not_none(start, end, periods) == 0:  # All of args is None
        end, periods = 'last', 30

    # Switch frequency Dayly, Weekly or Monthly
    freq_dict = {'D': jsm.DAILY, 'W': jsm.WEEKLY, 'M': jsm.MONTHLY}

    # 'first' means the start of recording date
    if start == 'first':
        data = jsm.Quotes().get_historical_prices(
            code, range_type=freq_dict[freq], all=True)
        start = [i.date for i in data][-1]
    else:
        data = None  # Temporaly defined

    # 'last' means last weekday (or today)
    if end == 'last':
        end = pd.datetime.today()

    # Return "start" and "end"
    start, end = (x.date() if hasattr(x, 'date')
                  else x for x in set_span(start, end, periods, freq))
    print('Get data from {} to {}'.format(start, end))

    data = jsm.Quotes().get_historical_prices(
        code, range_type=freq_dict[freq], start_date=start, end_date=end) if not data else data
    df = _convert_dataframe(data)
    return df[start:end]


def _convert_dataframe(target):
    """Convert <jsm.pricebase.PriceData> to <pandas.DataFrame>"""
    date = [_.date for _ in target]
    open = [_.open for _ in target]
    high = [_.high for _ in target]
    low = [_.low for _ in target]
    close = [_.close for _ in target]
    adj_close = [_._adj_close for _ in target]
    volume = [_.volume for _ in target]
    data = {'Open': open,
            'High': high,
            'Low': low,
            'Close': close,
            'Adj Close': adj_close,
            'Volume': volume}
    columns = *data.keys(),
    df = pd.DataFrame(data, index=date, columns=columns).sort_index()
    df.index.name = 'Date'
    return df
```

# pandas_datareaderと比較

## pandas_datareaderと異なる機能

* 銘柄はsymbol(Amazonなら'AMZN'とする)ではなく株価コード(数字4桁)で取得
* 週足、月足での取得も可能(freq='W', freq='M'オプション)
    * デフォルトは日足(freq='D')
* 期間の指定(periods=<数字>)で「endまたはstartから数えて<数字>本足の取得」
    * periodsを指定するとendまたはstartオプションが必ず必要です。
* デフォルトでは、今日から数えて30日前からの日足データの取得(`freq='D', end=pd.datetime.today(), periods=30`と同様)
    * pandas_datereaderではデフォルトで2010年1月1日から今日までの株価取得(`start=20100101, end=pd.datetime.today()`)




## pandas_datareaderの使用例
[pandas-datareader公式](https://pandas-datareader.readthedocs.io/en/latest/)
インストールは`$ conda install pandas-datareader`や`$ pip install pandas-datareader`




```python:pandas_datareaderによるamazonの株価取得
from pandas_datareader import data
end = pd.datetime.today()  # 今日の日付
start = (pd.Period(end, 'D')-300).start_time  # 300日前日付
df = data.get_data_yahoo('AMZN', end=end, start=start)  # 300日前からのAmazon株価取得
df.tail()
```


|   | Open  | High  | Low  | Close  | Adj Close  | Volume |
|:--|--:|--:|--:|--:|--:|--:|
| Date  |  |  |  |  |  |  |
| 2017-12-11  | 1164.599976  | 1169.900024  | 1157.000000  | 1168.920044  | 1168.920044  | 2363500 |
| 2017-12-12  | 1166.510010  | 1173.599976  | 1161.609985  | 1165.079956  | 1165.079956  | 2235900 |
| 2017-12-13  | 1170.000000  | 1170.869995  | 1160.270020  | 1164.130005  | 1164.130005  | 2616800 |
| 2017-12-14  | 1163.709961  | 1177.930054  | 1162.449951  | 1174.260010  | 1174.260010  | 3156600 |
| 2017-12-14  | 1163.709961  | 1177.930054  | 1162.449951  | 1176.089966  | 1176.089966  | 1873348 |