2019年12月13日 星期五

用深度學習預測股市範例實作參考與修改

python用了好一陣子,真的是很好用的工具
適逢其時,現在最火熱的AI,深度學習套件TensorFlow也是以python開發
既然都入門python了,怎麼能放過,自然是上網找了些範例來學學
只是TensorFlow的基礎概念不是那麼直觀
一開始就卡關了,加上工作繁忙,還是只能先以工作為主
去年從一個圖像文字識別範例完成後就放下了
直到今年開始接觸到了Keras
這個函式庫用起來直觀多啦
也才終於讓我入門到深度學習的大門
感謝Keras,讚嘆Keras


目前Keras已經納入TensorFlow的套件中,不用額外安裝
而且除了搭配TensorFlow使用之外
還自帶了一堆開源的神經網路可直接用
像是圖像識別用的RESNET50、VGG16等等
再搭配上TensorFlow內的開放訓練資料集
自學實作上真的不是大問題

不過學了一陣子都在玩圖像
最近開始對其它應用有興趣想試試
像是RNN(遞迴神經網路),這種對於有順序邏輯性的網路
可以處理翻譯,文章風格模仿產生與股票預測
「股票預測!!!」,這個我相當有興趣,就決定從這個方向來學
找了一下網路上相關的範例,就下面這個最詳細,而且針對單一股票處理
用深度學習幫你解析K線圖!
那就來試著玩玩看
程式環境上使用的python是3.6.9,tensorflow直接使用2.0.0
還需要額外安裝的函式庫有pandas,scikit-learn、numpy、matplotlib與requests

嗯?requests,上面那篇裡沒有提到網頁用的requests啊?
這是因為裡面訓練所需要的資料,要寫信請網誌主提供
先不要說我臉皮薄又懶得寫信
股票那麼多檔,總不能所有資料都靠別人給,終究還是要自己抓
所以在正式開始進入訓練前,先來想辦法撈一下我們想要的資料
而其實目前AI深度學習的第一步,也是最重要,最花時間的一步
還是從資料收集與處理做起
沒有正確地資料,也就訓練不出正確地答案
何況這資料也只要多打幾行程式就可以一直用下去
就先從這邊開始吧

目前從網路上找到的抓股市資料的範例大致上有兩種
一種是直接從公開資訊觀測站上面抓json下來
另一種是從股市網站抓網頁上的資料
JSON比較好處理,所以我是使用第一種辦法
根據找到的這個網站指示
只要在關鍵欄位加入年份、月份與股票代號,就可以抓那一整個月的股票資料
網站範例是直接存入MySQL,我們需要的是CSV檔,所以小改一下
另外,我們需要的資料的說明欄為了後續方便,一律轉英文處理
所以各欄對應如下:
日期(date),成交股數(day_shares),成交金額(day_price),開盤價(open),最高價(high),最低價(low),收盤價(close)
與漲跌價差(diff)跟成交筆數(volume)
我們以現在最火熱的台積電(2310)當訓練與第一驗證的目標好了
抓取的資料從2015年1月到2019年10月
def getStockCSV(date1, date2 ,stock_no):
  
  """產生範圍內的日期陣列:"""
  #製作空陣列
  date_data = []
  #取得起始日期,去掉不必要符號
  d1 = re.split(',|-|/',date1) 
  start_date = datetime.date(int(d1[0]),int(d1[1]),1)
  #取得結束日期,去掉不必要符號
  d2 = re.split(',|-|/',date2) 
  end_date = datetime.date(int(d2[0]),int(d2[1]),1)
  
  #利用雙迴圈產生日期陣列
  for i in range(int(d1[0]), int(d2[0])+1):
    for j in range(1, 13):
      count_date = datetime.date(i,j,1)
      if (count_date >= start_date) & (count_date <= end_date):
        date_data.append(datetime.date(i,j,1))
  
  """向股市網址要求資料"""
  url = 'http://www.twse.com.tw/exchangeReport/STOCK_DAY?date=%s&stockNo=%s' % ( start_date.strftime('%Y%m%d'), stock_no)
  r = requests.get(url)
  first_data = r.json()
  
  #如果資料欄中的stat是OK,代表有資料可以下載
  if(first_data['stat'] == 'OK'):
    print('股票存在')
    #建立csv檔
    csvfile1 = open(stock_no+'.csv','w', newline='\n', encoding = 'utf-8')
    writer1 = csv.writer(csvfile1)
    #轉英文後的欄位說明
    title=['date','days_shares','days_price','open','high','low','close','diff','volume']
    #建立CSV內的欄位
    writer1.writerow(title)
    #先關閉檔案
    csvfile1.close()
    print('檔案建立')
    
    """依日期正式下載股票資料"""
    for date in date_data:
      url = 'http://www.twse.com.tw/exchangeReport/STOCK_DAY?date=%s&stockNo=%s' % ( date.strftime('%Y%m%d'), stock_no)
      r = requests.get(url)
      stock_data = r.json()
      
      #開啟繼續寫檔模式
      csvfile = open(stock_no+'.csv','a', newline='\n', encoding = 'utf-8')
      writer = csv.writer(csvfile)
      for row in stock_data['data']:
        writer.writerow(row)
      csvfile.close()
      time.sleep(5) #避免連續要求被距絕,下載後暫停5秒,再下載下一筆
  else:
    print('找不到此股票,或日期錯誤')

getData("2015-01", "2019-10", "2330")

拿著這程式試著抓了幾間公司,發現有公司抓不到...
查了一下,抓不到的是上櫃的公司
再查了一下,這邊說,上櫃要從這裡抓
嗯,不但網址不同,連年份的格式也不同,這邊是民國...真麻煩
不過到也還好,至少資料格式除了日期以外都類似
反正就上市的一種抓法,上櫃的一種抓法
重新統整一下程式碼
就變成下面這樣,餵入年月,還有股票代號
如果有在上市找到了,就繼續抓資料,轉成CSV檔存下來
如果在上市區找不到,就變換年月格式,往上櫃網頁找,一樣轉成CSV存檔。
當然連上櫃都沒資料,那大概是輸入錯誤了
import time
import request
import csv
import datetime

def getStockCSV(date1, date2 ,stock_no):
  
  """產生範圍內的日期陣列:"""
  #製作空陣列
  date_data = []
  #取得起始日期,去掉不必要符號
  d1 = re.split(',|-|/',date1) 
  start_date = datetime.date(int(d1[0]),int(d1[1]),1)
  #取得結束日期,去掉不必要符號
  d2 = re.split(',|-|/',date2) 
  end_date = datetime.date(int(d2[0]),int(d2[1]),1)
  
  #利用雙迴圈產生日期陣列
  for i in range(int(d1[0]), int(d2[0])+1):
    for j in range(1, 13):
      count_date = datetime.date(i,j,1)
      if (count_date >= start_date) & (count_date <= end_date):
        date_data.append(datetime.date(i,j,1))
  
  """向股市網址要求資料"""
  url = 'http://www.twse.com.tw/exchangeReport/STOCK_DAY?date=%s&stockNo=%s' % ( start_date.strftime('%Y%m%d'), stock_no)
  r = requests.get(url)
  first_data = r.json()
  
  #如果資料欄中的stat是OK,代表有資料可以下載
  if(first_data['stat'] == 'OK'):
    print('股票存在')
    #建立csv檔
    csvfile1 = open(stock_no+'.csv','w', newline='\n', encoding = 'utf-8')
    writer1 = csv.writer(csvfile1)
    #轉英文後的欄位說明
    title=['date','days_shares','days_price','open','high','low','close','diff','volume']
    #建立CSV內的欄位
    writer1.writerow(title)
    #先關閉檔案
    csvfile1.close()
    print('檔案建立')
    
    """依日期正式下載股票資料"""
    for date in date_data:
      url = 'http://www.twse.com.tw/exchangeReport/STOCK_DAY?date=%s&stockNo=%s' % ( date.strftime('%Y%m%d'), stock_no)
      r = requests.get(url)
      stock_data = r.json()
      
      #開啟繼續寫檔模式
      csvfile = open(stock_no+'.csv','a', newline='\n', encoding = 'utf-8')
      writer = csv.writer(csvfile)
      for row in stock_data['data']:
        writer.writerow(row)
      csvfile.close()
      time.sleep(5) #避免連續要求被距絕,下載後暫停5秒,再下載下一筆
  else:
    """取得上櫃股票資料"""
    #產生民國格式的日期
    cstart_date = str(int(start_date.strftime('%Y'))-1911)+'/'+start_date.strftime('%m')
    
    """向上櫃股票網頁要求資料"""
    url = 'https://www.tpex.org.tw/web/stock/aftertrading/daily_trading_info/st43_result.php?d=%s&stkno=%s' % ( cstart_date, stock_no)
    r = requests.get(url)
    first_data = r.json()
    
    #如果資料欄中的iTotalRecards不是0,代表有資料可以下載
    if first_data['iTotalRecords'] > 0:
      print('上櫃股票存在')
      #建立csv檔
      csvfile1 = open(stock_no+'.csv','w', newline='\n', encoding = 'utf-8')
      #建立CSV內的欄位
      writer1 = csv.writer(csvfile1)
      #轉英文後的欄位說明
      title=['date','days_shares','days_price','open','high','low','close','diff','volume']
      writer1.writerow(title)
      #先關閉檔案
      csvfile1.close()
      print('上櫃檔案建立')
      
      """依日期正式下載股票資料"""
      for date in date_data:
        #產生民國格式的日期
        cdate = str(int(date.strftime('%Y'))-1911)+'/'+date.strftime('%m')
        url = 'https://www.tpex.org.tw/web/stock/aftertrading/daily_trading_info/st43_result.php?d=%s&stkno=%s' % ( cdate, stock_no)
        r = requests.get(url)
        stock_data = r.json()
        
        #開啟繼續寫檔模式
        csvfile = open(stock_no+'.csv','a', newline='\n', encoding = 'utf-8')
        writer = csv.writer(csvfile)
        for row in stock_data['aaData']:
          writer.writerow(row)
        csvfile.close()
        time.sleep(5) #避免連續要求被距絕,下載後暫停5秒,再下載下一筆
    else:
      print('找不到此股票,或日期錯誤')

getData("2015-01", "2019-10", "2330")
上櫃這邊,我抓了威剛(3260)一樣是2015年1月到2019年10月的資料
準備當作第二驗證的目標

總於要開始進行實際的深度學習程式撰寫啦
但第一步,還是要先處理資料
範例裡面,是用pandas抓取csv的資料
import pandas as pd

foxconndf= pd.read_csv('./2330.csv', index_col=0 , thousands=",")
foxconndf.dropna(how='any',inplace=True)
#去除CSV裡不要的資料
foxconndf.drop(['days_shares','days_price','diff'], axis=1, inplace=True)
而這個變數名稱,是鴻海公司,想改也行
不過這是抄人家的東西,能不動就先不動吧
與原版不一樣的,有兩項
第一,讀檔時加入了thousands=","的引數
這是因為引入的資料裡有逗號,如果不使用這個參數
那麼用pandas讀入時就會當字串處理
到時還要再處理一次,不如就在讀入時一口氣處理
第二,去掉成交股數,成交金額與漲跌價差的資料
因為這些都不用到,避免運算上的麻煩,就乾脆不用比較好
沒辦法,我們不是用原作者的資料,還是要小處理一下

接下來就是正規化資料。
正規化簡單地說,就是把最大值壓在1,最小值控制在0的運算
做正規化最主要原因就是要讓輸入資料變化量都差不多。
不然像成交筆數熱門時通常都是股數的好幾倍
會讓訓練模型時,太偏重這些資料,最後導致失真
這邊使用scikit-learn的套件進行正規化
from sklearn import preprocessing

def normalize(df):
    newdf= df.copy()
    min_max_scaler = preprocessing.MinMaxScaler()
    
    newdf['open'] = min_max_scaler.fit_transform(df.open.values.reshape(-1,1))
    newdf['low'] = min_max_scaler.fit_transform(df.low.values.reshape(-1,1))
    newdf['high'] = min_max_scaler.fit_transform(df.high.values.reshape(-1,1))
    newdf['volume'] = min_max_scaler.fit_transform(df.volume.values.reshape(-1,1))
    newdf['close'] = min_max_scaler.fit_transform(df.close.values.reshape(-1,1))
    
    return newdf

foxconndf_norm= normalize(foxconndf)

接著是產生訓練與驗證用的資料
原作者命名為Data_Helper的函數
裡面做的處理的概念很簡單
它要用n天的資料,當訓練集,用n+1天的收盤價當答案
所以舉例來說,我的n用10天
那就是抓1~10天的各項資料做第一筆訓練資料,第11天的收盤價當答案
然後抓2~11天的各項資料做第二筆訓練資料,第12天的收盤價當答案
依此類推,如果有60天的股票資料,用10天當區間,就會產生49筆的訓練資料
有這樣的結果後,再來抓9成的資料當訓練資料集,1成的資料做成驗證資料集
概念簡單歸簡單,實際上這運算要自己寫還是蠻複雜的
感謝原作者寫的範例,讓我們照著學習

只是範例終究還是參考用而已,這部分還是不能完全套用
最主要是因為我用的訓練資料是自己抓的
如果我們不做正規化,直接把資料送進原始的Data_Helper裡
看y_train與y_test的資料會發現,取得的其實是成交筆數(volume)的資料
而不是收盤價(close)
因為資料原始順序會是open,high,low,close,volume的排列
y_train = result[:int(number_train), -1][:,-1]
取得是資料是最後一欄volume
這時候要取得倒數第二欄close,要改成
y_train = result[:int(number_train), -1][:,-2]
y_test的部分也是一樣,要改成取-2,而不是-1
我一開始沒改程式照著跑時,出來的結果一點都不符合
亂改之下,甚至還有出現預測結果就是條水平線
輸入資料正確與否,真的很重要啊
修改好後要決定天數的部分,暫時就先以範例的20天為準
import numpy as np

def data_helper(df, time_frame):
    
    # 資料維度: 開盤價、收盤價、最高價、最低價、成交量, 5維
    number_features = len(df.columns)

    # 將dataframe 轉成 numpy array
    datavalue = df.as_matrix()

    result = []
    # 若想要觀察的 time_frame 為20天, 需要多加一天做為驗證答案
    for index in range( len(datavalue) - (time_frame+1) ): # 從 datavalue 的第0個跑到倒數第 time_frame+1 個
        result.append(datavalue[index: index + (time_frame+1) ]) # 逐筆取出 time_frame+1 個K棒數值做為一筆 instance
    
    result = np.array(result)
    number_train = round(0.9 * result.shape[0]) # 取 result 的前90% instance做為訓練資料
    
    x_train = result[:int(number_train), :-1] # 訓練資料中, 只取每一個 time_frame 中除了最後一筆的所有資料做為feature
    y_train = result[:int(number_train), -1][:,-2] # 訓練資料中, 取每一個 time_frame 中最後一筆資料的最後一個數值(收盤價)做為答案
    
    # 測試資料
    x_test = result[int(number_train):, :-1]
    y_test = result[int(number_train):, -1][:,-2]
    
    # 將資料組成變好看一點
    x_train = np.reshape(x_train, (x_train.shape[0], x_train.shape[1], number_features))
    x_test = np.reshape(x_test, (x_test.shape[0], x_test.shape[1], number_features))  

    return [x_train, y_train, x_test, y_test]

# 以20天為一區間進行股價預測
X_train, y_train, X_test, y_test = data_helper(foxconndf_norm, 20)

接下來就不太有什麼大修改了
只是因為我用Tensorflow是2.0的版本
所以依新版的需求再改了一下import的部分
一樣還是使用keras建我們需要的模型
模型建了2個LSTM層與2個一般神經網路
中間加了drop參數,濾掉一點連結性
嗯~~剛接觸看不懂上面的說明對吧
先丟掉複雜的理論說明,簡單地說
LSTM全名(Logn Short-Term Memory)是一種RNN的架構
然後深度學習的一個重點就是它是一種多層的神經網路結合在一起
所以,在程式上的處理就像這個程式碼一樣
建構一層又一層自己設計的網路
然後給定loss function與optimize的參數,也就是決定尋找答案的方式
這樣就完成了模型的建立
最後把數值餵給模型進行訓練
當然訓練也有參數,一次進行訓練,總共訓練幾次等等
就可以等等等等,等個一陣子把結果算出來
有結果後就先存檔,等等還可以繼續用
#改成以tensorflow 2.0的導入方式
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Dropout, Activation
from tensorflow.keras.layers import LSTM
import tensorflow.keras as keras
from tensorflow.keras import optimizers

def build_model(input_length, input_dim):
    d = 0.3
    model = Sequential()  #建立神經網路物件
    #建立一個LSTM網路層,256個神經元,輸入資料長度與輸入的維度,要連結下一個LSTM
    model.add(LSTM(256, input_shape=(input_length, input_dim), return_sequences=True))
    model.add(Dropout(d))  #去除連結
    #建立一個LSTM網路層,256個神經元,輸入資料長度與輸入的維度,不連結下一個LSTM
    model.add(LSTM(256, input_shape=(input_length, input_dim), return_sequences=False)) 
    #建立一個一般神經網路層,16個神經元,建立初始化,激勵函數是relu
    model.add(Dense(16,kernel_initializer="uniform",activation='relu'))
    #建立一個一般神經網路層,1個神經元,建立初始化,激勵函數是線性函數,做為輸出用
    model.add(Dense(1,kernel_initializer="uniform",activation='linear'))
    #決定模型的尋找答案方式,Loss函數:均方誤差(Mean Square Error,ADAM
    model.compile(loss='mse', optimizer='adam', metrics=['accuracy'])
    return model

#建立20天期間,5組數據輸入的神經網路
model = build_model( 20, 5 )

#針對神經網路開始訓練,一次128組一起訓練,訓練50次,以訓練集的1成驗證,顯示訓練進度
model.fit( X_train, y_train, batch_size=128, epochs=50, validation_split=0.1, verbose=1 )
#把訓練後的結果連同整個模型存下來
model.save('LSTM_tf2_20.h5')

因為這模型實際輸出是正規化後的結果
所以還是要去正規化,還原成實際的數值才是我們想看的東西
原作者很貼心地寫了這樣的函數
把測試用的資料,餵給這個模型算出預測的結果
再用這樣的函數解出實際的大小
#去正規化轉換
def denormalize(df, norm_value):
    original_value = df['close'].values.reshape(-1,1)
    norm_value = norm_value.reshape(-1,1)

    min_max_scaler = preprocessing.MinMaxScaler()
    min_max_scaler.fit_transform(original_value)

    denorm_value = min_max_scaler.inverse_transform(norm_value)
    return denorm_value

# 用訓練好的 LSTM 模型對測試資料集進行預測
pred = model.predict(X_test)

# 將預測值與正確答案還原回原來的區間值
denorm_pred = denormalize(foxconndf, pred)
denorm_ytest = denormalize(foxconndf, y_test)

最後用這個數據跟測試集的答案畫圖作比對
然後我們幫這個圖加個標題,就用2330 TSMC
import matplotlib.pyplot as plt
#%matplotlib inline  

plt.plot(denorm_pred,color='red', label='Prediction')
plt.plot(denorm_ytest,color='blue', label='Answer')
plt.legend(loc='best')
plt.title('2330 TSMC') #加個圖表標題
plt.show()
出來就像這樣
耶,好像蠻符合的

我們抓了第二種股票,威鋼來試試的啦
重新修改程式碼,砍掉模型建立與訓練
直接載入剛剛訓練的結果
嗯,差不多,曲線是接近,但仍有相當差距
不過威剛與台積電算相近的產業
來試試其它的吧,再找兩個好了
一個和泰汽車,一個找上櫃的寶雅
和泰汽車OK,跑出來是這樣
然後寶雅就卡關了...
因為csv裡面插了一行帶有「-」的資料
pandas依這個資料,把整個資料都判定為文字而不是數字
自然就卡關了...
要靠寫程式把這東西處理掉有點麻煩(懶得再動腦)
直接打開notepad++,把這行幹掉再餵,這次OK了
兩個都還行, 不過終究上不了台面

我們改一下模型好了
LSTM層第一層改為512個神經元
一般神經網路層的第一層改神經元到256
dropout用0.2處理
然後修改訓練資料量,讓20天的資料縮減為10天
batch_size改成8,epochs改成100
重新用台積電的數據再來訓練一次
哇!更準了
一樣套用給威剛、和泰汽車與寶雅


也是很合,但無實用性...

先不講這條線與原本的線還是有相當偏差
抓了前10天的資料,只能預測第11天的結果
也就是再怎麼準,今天收盤後頂多就知道明天的收盤價
都收盤了,那就買不到,明天漲再多都沒用 XD
就算打定主意明天買,又猜不到後天,甚至未來的走勢
講講還真的蠻沒用的

不過對於學習上,當個入門學習範例沒問題
去學著怎麼調整模型與訓練,來達到想要的結果
還有確認訓練出來模型的泛用性與可靠性到底O不OK
之後再去思考,怎麼重新設計一個符合股票預測的新模型
這個只是個學習入門範例啦

沒有留言: