使用 pandas 玩轉股票數據

pandas 是數據分析的瑞士軍刀。我們今天使用 pandas 來玩一下股票數據,看看能從數據裏得到哪些有意思的信息。

pandas 教程

如果你熟悉 Python 的話,官網上的 10 Minutes to pandas 可以讓你在短時間內瞭解 pandas 能幹什麼事以及是怎麼幹的。針對每個主題,都可以橫向查到大量的資料和例子。

如果你 Python 不熟,但又想用 pandas 玩轉數據分析的話,Python for Data Analysis 是本不錯的書。書裏作者使用美國新生兒的名字得出了一些很有意思的結論。還分析了 movielen 的電影評分數據。熟悉 SQL 的同學應該對這些分析會深有感觸,相信這些人用 SQL 寫出過這些分析過程類似的代碼。這本書的缺點是有點囉嗦,如果你熟悉 Python 又想快速學習的話,看第二章就夠了。但這本書很適合不熟悉 Python 的人,書的最後一章還附了 Python 的教程,即如果只玩 pandas 的話,掌握這些 Python 知識就夠了,真夠貼心。而且本書的作者就是 pandas 的作者。

另外補充一點,最好使用 ipython 環境來玩轉數據分析。特別是 ipython notebook ,熟悉快捷鍵後,用起來會很順手。本文玩轉的股票數據就是使用 ipython notebook。

股票數據下載

搜索 ghancn 可以免費下載 2009 年之前的 5 分鐘數據和 1 分鐘數據。坦白講,數據質量不高,裏面有不少錯誤。但不影響我們玩這些數據。數據是以年爲單位分不同的文件夾保存的。

我們先看一下某個股票的數據長什麼樣:

import pandas as pd
import numpy as np


names = ['date',
         'time',
         'opening_price',
         'ceiling_price',
         'floor_price',
         'closing_price',
         'volume',
         'amount']
# 讀取數據時,我們以日期爲索引,並解析成日期格式
raw = pd.read_csv('raw/2008/SH600690.csv', names=names, header=None, index_col='date', parse_dates=True)
raw.head()
             time  opening_price  ceiling_price  floor_price  closing_price   volume    amount
date
2008-01-02  09:35          22.50          22.63        22.50          22.51   2042.50   4604723
2008-01-02  09:40          22.51          22.51        22.29          22.37   1545.17   3460503
2008-01-02  09:45          22.39          22.62        22.38          22.62   1744.76   3921443
2008-01-02  09:50          22.60          23.00        22.60          22.95   5339.00   12225939
2008-01-02  09:55          22.98          23.20        22.89          23.20   12577.73  28947824

轉化爲日交易數據

我們使用 2007 年和 2008 年的數據來作爲示例。因爲我們更關心是一些長期的趨勢,分鐘級別的交易數據太細了,我們轉換爲日數據。

# 股票漲跌幅檢查,不能超過 10% ,過濾掉一些不合法的數據
def _valid_price(g):
    return (((g.max() - g.min()) / g.min()) < 0.223).all()

# 按照日期分組
days = raw.groupby(level=0).agg(
    {'opening_price': lambda g: _valid_price(g) and g[0] or 0,
     'ceiling_price': lambda g: _valid_price(g) and np.max(g) or 0,
     'floor_price': lambda g: _valid_price(g) and np.min(g) or 0,
     'closing_price': lambda g: _valid_price(g) and g[-1] or 0,
     'volume': 'sum',
     'amount': 'sum'})
days.head()
            floor_price opening_price   ceiling_price   volume      amount      closing_price
date
2008-01-02  22.29       22.50           24.50           200809.34   476179680   24.03
2008-01-03  23.81       24.03           25.20           166037.98   406906304   24.54
2008-01-04  23.68       24.53           24.76           149078.64   358418560   24.17
2008-01-07  23.75       24.03           24.75           93950.43    227289136   24.38
2008-01-08  23.49       24.38           24.38           149056.24   355752416   23.53

這裏只是爲了玩這些數據,如果你真的需要股票日數據,雅虎財經網站上有質量非常高的日交易數據可供下載。

按照上述方法,可以把一個股票幾年的數據合併起來,生成一個包含所有年份的歷史日交易數據。具體可以參閱 stock.py 裏的 minutes_to_days_batch 函數。

股票波動率

什麼股票是好股票?要回答這個問題,先要把最簡單的問題說清楚。炒股就是低買高賣,實現獲利。那麼好股票的標準就是在你的持股週期內,波動最大的股票。這很好理解吧,波動最大,我們纔有可能在相對低點買入,在相對高點賣出,獲利最大。

在一定的時間週期內,衡量股票波動的指標定義爲 最高價/最低價。以我們表格中的數據,就是 ceiling_price/floor_price。這個比率最大的股票就是好股票。關於時間週期,這個和炒股策略有關。有些人喜歡做短線,可能就持股幾天,或一兩週。有些人習慣做長線,可能持股幾個月甚至幾年。也有些人本來打算做短線,做着做着變成長線,再做着做着,變成了股東。

爲了簡單起見,我們拿波動週期爲 30 個自然日來計算,即如果某個股票停牌,那麼他的價格就一直沒有變化,則波動爲 0。
這裏,我們直接使用 600690 這個股票來作爲示例。我們直接讀取已經合併過日交易的數據。

qdhr = pd.read_csv('test-data/SH600690.csv', index_col='date', parse_dates=True)
qdhr.head()

                floor_price     opening_price   ceiling_price   volume      amount      closing_price
date
2007-01-04      9.28            9.30            10.14           259264.75   254734000   9.80
2007-01-05      9.53            9.70            10.15           171169.97   170154432   9.90
2007-01-08      9.93            9.93            10.78           159340.58   164954896   10.60
2007-01-09      10.08           10.68           11.15           227163.31   246309216   10.55
2007-01-10      10.26           10.49           11.13           232858.18   246221520   11.10

我們發現數據中間有空洞,即週末和停牌時間裏是沒有數據的。我們把這些數據填充完整,我們看看 pandas 如何處理 missing data 。

填充數據

我們先生成一段連續的日期數據作爲索引:

# 填充數據:生成日期索引
l = len(qdhr)
start = qdhr.iloc[0:1].index.tolist()[0]
end = qdhr.iloc[l - 1: l].index.tolist()[0]
idx = pd.date_range(start=start, end=end)
idx
DatetimeIndex(['2007-01-04', '2007-01-05', '2007-01-06', '2007-01-07',
               '2007-01-08', '2007-01-09', '2007-01-10', '2007-01-11',
               '2007-01-12', '2007-01-13',
               ...
               '2008-12-22', '2008-12-23', '2008-12-24', '2008-12-25',
               '2008-12-26', '2008-12-27', '2008-12-28', '2008-12-29',
               '2008-12-30', '2008-12-31'],
               dtype='datetime64[ns]', length=728, freq='D')

接着使用 reindex 函數缺失的數據被全。填充股票數據時有個要求,我們把缺失的價格數據用前一個交易日的數據來填充,但交易量需要填充爲 0。

data = qdhr.reindex(idx)
zvalues = data.loc[~(data.volume > 0)].loc[:, ['volume', 'amount']]
data.update(zvalues.fillna(0))
data.fillna(method='ffill', inplace=True)
data.head()
            floor_price opening_price   ceiling_price   volume      amount      closing_price
2007-01-04  9.28        9.30            10.14           259264.75   254734000   9.8
2007-01-05  9.53        9.70            10.15           171169.97   170154432   9.9
2007-01-06  9.53        9.70            10.15           0.00        0           9.9
2007-01-07  9.53        9.70            10.15           0.00        0           9.9
2007-01-08  9.93        9.93            10.78           159340.58   164954896   10.6

我們可以看到,06, 07 兩天的數據被正確地填充了。

分組計算

我們需要計算 30 個自然日裏的股票平均波動週期。這樣,我們必須以 30 天爲單位,對所有的歷史數據進行分組。然後逐個分組計算其波動率。

生成分組索引

# 定義產生分組索引的函數,比如我們要計算的週期是 20 天,則按照日期,20 個交易日一組
def gen_item_group_index(total, group_len):
    """ generate an item group index array

    suppose total = 10, unitlen = 2, then we will return array [0 0 1 1 2 2 3 3 4 4]
    """

    group_count = total / group_len
    group_index = np.arange(total)
    for i in range(group_count):
        group_index[i * group_len: (i + 1) * group_len] = i
    group_index[(i + 1) * group_len : total] = i + 1
    return group_index.tolist()
In [7]: gen_item_group_index(10, 3)
Out [7]: [0, 0, 0, 1, 1, 1, 2, 2, 2, 3]

根據分組索引來分組

period = 30

group_index = gen_item_group_index(len(data), period)
# 把分組索引數據添加到股票數據裏
data['group_index'] = group_index
print len(data)
data.head().append(data.tail())

我們看一下添加了分組索引後的數據最前面 5 個和最後 5 個數據,注意 group_index 的值。我們接下來就是根據這個值進行分組。

            floor_price opening_price   ceiling_price   volume      amount      closing_price   group_index
2007-01-04  9.28        9.30            10.14           259264.75   254734000   9.80            0
2007-01-05  9.53        9.70            10.15           171169.97   170154432   9.90            0
2007-01-06  9.53        9.70            10.15           0.00        0           9.90            0
2007-01-07  9.53        9.70            10.15           0.00        0           9.90            0
2007-01-08  9.93        9.93            10.78           159340.58   164954896   10.60           0
2008-12-27  8.97        9.15            9.23            0.00        0           9.08            24
2008-12-28  8.97        9.15            9.23            0.00        0           9.08            24
2008-12-29  8.73        9.04            9.15            38576.07    34625144    9.11            24
2008-12-30  8.95        9.14            9.14            62983.38    56876600    8.96            24
2008-12-31  8.95        9.00            9.11            32829.30    29620508    8.99            24

分組計算最高價和最低價

# 針對下跌的波動,我們把最高價設置爲負數。什麼是下跌的波動?就是先出現最高價,再出現最低價
def _ceiling_price(g):
    return g.idxmin() < g.idxmax() and np.max(g) or (-np.max(g))


# 根據索引分組計算
group = data.groupby('group_index').agg({
                                        'volume': 'sum',
                                        'floor_price': 'min',
                                        'ceiling_price': _ceiling_price})
group.head()
                volume      ceiling_price   floor_price
group_index
0               1271711.00  22.33           16.21
1               1831018.01  24.75           18.98
2               2038944.01  -27.20          20.08
3               477219.16   23.49           21.40
4               203932.07   -22.48          20.10

給每個分組添加起始日期

有時我們看到某個週期內下跌了很多,或上漲了很多,我們想知道是什麼時候發生的,所以需要給每個分組添加起始日期。

# 添加每個分組的起始日期
date_col = pd.DataFrame({"group_index": group_index, "date": idx})
group['date'] = date_col.groupby('group_index').agg('first')
group.head()

idx 是我們在上面代碼裏生成的連續的日期索引數據。添加日期數據後的樣子:

                volume      ceiling_price   floor_price     date
group_index
0               4634226.68  -12.38          9.02            2007-01-04
1               3499001.47  11.64           8.80            2007-02-03
2               6061972.34  12.79           9.41            2007-03-05
3               6086797.19  15.50           12.00           2007-04-04
4               5687407.73  17.15           13.49           2007-05-04

添加波動率

# 添加我們的波動指標 股票波動係數 = 最高價/最低價
group['ripples_radio'] = group.ceiling_price / group.floor_price
group.head()
                volume          ceiling_price   floor_price     date            ripples_radio
group_index
0               4634226.68      -12.38          9.02            2007-01-04      -1.372506
1               3499001.47      11.64           8.80            2007-02-03      1.322727
2               6061972.34      12.79           9.41            2007-03-05      1.359192
3               6086797.19      15.50           12.00           2007-04-04      1.291667
4               5687407.73      17.15           13.49           2007-05-04      1.271312

排序

按照波動率排序,可以看到某段時間內波動最大的一些時間段。

# 降序排列。我們把分組的起始日期,交易量總和都列出來,也可以觀察一下交易量和股票波動比的關係
ripples = group.sort_values('ripples_radio', ascending=False)
ripples.head()
            volume          ceiling_price   floor_price     date            ripples_radio
group_index
101         4352881.31      14.85           9.18            2008-04-21      1.617647
90          5703121.25      18.89           11.85           2007-05-27      1.594093
92          4545365.71      23.96           16.42           2007-07-26      1.459196
85          4126972.83      12.38           8.58            2006-12-28      1.442890
84          2952951.46      9.20            6.40            2006-11-28      1.437500

從數據可以看出來,波動最大的在 30 個自然日內上漲了 61.76%。發生在 2008-04-21 開始的 30 天內。

當然,我們也可以計算前 10 大上漲波動的平均值。

ripples.head(10).ripples_radio.mean()
1.3657990069195818

也可以計算前 10 大下跌波動的平均值。

ripples.tail(10).ripples_radio.mean()
-1.4124407127785106

看來下跌的平均值比上漲的還大呀。

我們針對每個股票都使用上述方法計算其平均波動,這樣我們就可以從一系列股票裏找出那些波動最大的股票了。當然,上漲波動越大,下跌波動也越大,正所謂風險和機遇並存嘛。具體可參閱 stock.py 裏的 stock_ripples_batch 函數。

其他玩法

計算漲跌幅

我們注意到原始數據裏沒有漲跌幅的數據。漲跌幅定義爲今日收盤價減去昨日收盤價。我們換個股票,取出原始數據。

data = pd.read_csv('test-data/SZ000565.csv', index_col='date', parse_dates=True)
data.head()
            floor_price     opening_price   ceiling_price   volume      amount          closing_price
date
2007-01-04  4.16            4.22            4.27            17877.88    7477370.52      4.19
2007-01-05  4.15            4.16            4.27            10857.66    4588246.02      4.24
2007-01-08  4.27            4.27            4.45            30770.01    13467986.00     4.44
2007-01-09  4.42            4.48            4.54            26276.89    11726492.00     4.45
2007-01-10  4.36            4.45            4.90            80840.76    37866240.01     4.90

利用 diff 函數快速計算漲跌幅。

rise = data.closing_price.diff()
data['rise'] = rise
data.head()
    floor_price opening_price   ceiling_price   volume  amount  closing_price   rise
date
2007-01-04  4.16    4.22    4.27    17877.88    7477370.52  4.19    NaN
2007-01-05  4.15    4.16    4.27    10857.66    4588246.02  4.24    0.05
2007-01-08  4.27    4.27    4.45    30770.01    13467986.00 4.44    0.20
2007-01-09  4.42    4.48    4.54    26276.89    11726492.00 4.45    0.01
2007-01-10  4.36    4.45    4.90    80840.76    37866240.01 4.90    0.45

注意到第一條記錄的漲跌幅爲 NaN,因爲第一條記錄的昨日是沒有數據的。感興趣的同學可以再計算一下漲跌百分比,其定義爲當日的漲跌幅除以昨日的收盤價。

計算指定時間點之前的一段時間內波動最大的股票

有時我們關心某個時間點之前的一段時間變化最劇烈的股票。比如最近一週漲幅最大的,最近一週跌幅最大的,或者最近一個月交易量變化最大的等等。

我們看一下 000565 這個股票在 2008-12-31 之前 30 個自然日裏的波動率。

選定數據

這裏涉及到用日期對數據進行分片的技術,我們需要選擇指定日期及之前一段時間內的數據。

end_date = '2008-12-31'
period = 30

end_date = pd.Timestamp(end_date)
start_date = end_date - pd.Timedelta(days=period)

data = pd.read_csv('test-data/SZ000565.csv', index_col='date', parse_dates=True)
data = data.loc[start_date:end_date]
data
            floor_price     opening_price   ceiling_price   volume          amount          closing_price
date
2008-12-01  7.40            7.58            7.90            41747.12        3.214610e+07    7.88
2008-12-02  7.55            7.56            8.38            74552.15        6.029661e+07    8.32
2008-12-03  8.40            8.40            8.93            85361.64        7.420082e+07    8.82
2008-12-04  8.42            8.88            9.08            110410.46       9.740610e+07    8.50
2008-12-05  8.33            8.40            9.35            126479.91       1.133572e+08    9.35
2008-12-08  9.35            9.40            9.99            149491.39       1.436038e+08    9.69
2008-12-09  9.10            9.73            9.73            89871.90        8.405230e+07    9.15
2008-12-10  9.09            9.11            9.55            70036.94        6.571389e+07    9.46
2008-12-11  9.06            9.40            9.47            57735.24        5.328468e+07    9.06
2008-12-12  8.15            8.80            9.00            59210.49        5.038026e+07    8.29
2008-12-15  8.30            8.33            8.72            41758.27        3.534860e+07    8.50
2008-12-16  8.02            8.48            8.60            38808.62        3.220561e+07    8.60
2008-12-17  8.58            8.67            8.89            46993.48        4.114008e+07    8.65
2008-12-18  8.50            8.62            8.81            34061.97        2.965074e+07    8.78
2008-12-19  8.79            8.79            9.39            70327.47        6.435001e+07    9.18
2008-12-22  8.95            9.19            9.39            50195.75        4.592311e+07    9.11
2008-12-23  8.20            9.17            9.17            75732.72        6.507140e+07    8.20
2008-12-24  7.59            8.03            8.18            61498.16        4.823624e+07    7.82
2008-12-25  7.40            7.90            7.93            34791.00        2.672370e+07    7.52
2008-12-29  6.96            7.50            7.55            31694.04        2.274100e+07    7.26
2008-12-30  7.11            7.29            7.48            25533.01        1.865500e+07    7.15
2008-12-31  6.94            7.16            7.25            22324.32        1.577828e+07    6.95

選出數據後,計算波動率就簡單了。我們按照老辦法,上漲的波動率爲正數,下跌的波動率爲負數。

# 計算波動值
_ripple_radio = lambda data: data.ceiling_price.max() / data.floor_price.min()
ripple_radio = data.floor_price.idxmin() < data.ceiling_price.idxmax() and _ripple_radio(data) or -_ripple_radio(data)
ripple_radio
-1.4394812680115274

最後,遍歷所有的股票,計算其指定日期之前的一段時間的波動值,選出波動最大的股票,即是我們關注的股票。比如,經歷股票大跌,我們判斷會反彈,我們想搶反彈,搶哪個股票呢?答案是搶大跌中下跌最多的,因爲下跌最多的股票往往反彈也最多。這部分代碼可參閱 stock.py 裏的 recent_ripples 函數。

爲什麼要用 pandas 玩轉股票數據

答案應該已經比較明顯了,雖然很多數據股票軟件裏都有。但一些高級的數據篩選方式其實這些股票軟件都不支持的。最後,需要補充一句,大家都是成年人,文章裏的任何策略是個人的思路,不構成投資建議啊,後果自負啊。

最最後,感興趣的可以看一下 stock.ipynb,這個是本文在 ipython notebook 環境下的所有代碼。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章