資料科學在量化金融中的應用:指數預測(下)

語言: CN / TW / HK

回顧《資料科學在量化金融中的應用:指數預測(上)》,我們對股票指數資料進行了收集、探索性分析和預處理。接下來,本篇會重點介紹特徵工程、模型選擇和訓練、模型評估和模型預測的詳細過程,並對預測結果進行分析總結。

特徵工程

在正式建模之前,我們需要對資料再進行一些高階處理 — 特徵工程,從而保證每個變數在模型訓練中的公平性。根據現有資料的特點,我們執行的特徵工程流程大致有以下三個步驟:

  • 處理缺失值並提取所需變數
  • 資料標準化
  • 處理分類變數

1.  處理缺失值並提取所需變數

首先,我們需要剔除包含缺失值的行,並只保留需要的變數 x_input,為下一步特徵工程做準備。

x_input = (df_model.dropna()[['Year','Month','Day','Weekday','seasonality','sign_t_1','t_1_PricePctDelta','t_2_PricePctDelta','t_1_VolumeDelta']].reset_index(drop=True))
x_input.head(10)

然後,再將目標預測列 y 從資料中提取出來。

y = df_model.dropna().reset_index(drop=True)['AdjPricePctDelta']

2.  資料標準化

由於價格百分比差與交易量差在數值上有很大差距,如果不標準化資料,可能導致模型對某一個變數有傾向性。為了平衡各個變數對於模型的影響,我們需要調整除分類變數以外的資料,使它們的數值大小相對近似。Python 提供了多種資料標準化的工具,其中 sklearn 的 StandardScaler 模組比較常用。資料標準化的方法有多種,我們選擇的是基於均值和標準差的標準化演算法。這裡,大家可以根據對資料特性的理解和模型型別的不同來決定使用哪種演算法。比如對於樹形模型來說,標準化不是必要步驟。

scaler = StandardScaler()
x = x_input.copy()
x[['t_1_PricePctDelta','t_2_PricePctDelta','t_1_VolumeDelta']]=scaler.fit_transform(x[['t_1_PricePctDelta','t_2_PricePctDelta','t_1_VolumeDelta']])

3.  處理分類變數

最常見的分類變數處理方法之一是 one-hot encoding。對於高基數的分類變數,經過編碼處理後,變數數量增加,大家可以考慮通過降維或更高階的演算法來降低計算壓力。

x_mod = pd.get_dummies(data=x, columns=['Year','Month','Day','Weekday','seasonality'])
x_mod.columns

x_mod.shape

至此,我們完成了特徵工程的全部步驟,處理後的資料就可以進入模型訓練環節了。

模型選擇和訓練

首先,我們需要拆分訓練集和測試集。對於不需要考慮記錄順序的資料,可以隨機選取一部分資料作為訓練集,剩下的部分作為測試集。而對於時間序列資料來說,記錄之間的順序是需要考慮的,比如我們想要預測2月份的價格變動,那麼模型就不能接觸2月份以後的價格,以免資料洩露。由於股票指數資料為時間序列,我們將時間序列前75%的資料設為訓練資料,後25%的資料設為測試資料。

· 模型選擇

在模型選擇階段,我們會根據資料的特點,初步確定模型方向,並選擇合適的模型評估指標。

因為變數中包含歷史價格和交易量,且這些變數的相關性過高(high correlation),以線性模型為基礎的各類迴歸模型並不適合目標資料。因此,我們模型嘗試的重心將放在整合方法(ensemble method),以這類模型為主。

在訓練過程中,我們需要酌情考慮,選擇合適的指標來評估模型表現。對於迴歸預測模型而言,比較流行的選擇是 MSE(Mean Squared Error)。而對於股票指數資料來說,由於其時間序列的特性,我們在 RMSE 的基礎上又選擇了 MAPE(Mean Absolute Percentage Error),一種相對度量,以百分比為單位。比起傳統的 MSE,它不受資料大小的影響,數值保持在0-100之間。因此,我們將 MAPE 作為主要的模型評估指標。

· 模型訓練

在模型訓練階段,所有的候選模型將以預設引數進行訓練,我們根據 MAPE 的值來判斷最適合進一步細節訓練的模型型別。我們嘗試了包括線性迴歸、隨機森林等多種模型演算法,並將經過訓練集訓練的各模型在測試集中的模型表現以字典的形式列印返回。

模型評估

通過執行以下方程,我們可以根據預測差值(MAPE)的大小對各模型的表現進行排列。大家也可以探索更多種不同的模型,根據評估指標的高低擇優選取模型做後續微調。

trail_result = ensemble_method_reg_trails(x_train, y_train, x_test, y_test)

pd.DataFrame(trail_result).sort_values('model_test_mape', ascending=True)

由此可以看出,在眾多模型型別中,Ada Boost 在訓練和測試集上的效果最好,MAPE 值最小,所以我們選擇 Ada Boost 進行下一步的細節調優。與此同時,我們發現 random forest 和 gradient boosting 也有不錯的預測表現。注意,Ada Boost 雖然在訓練集上準確度高,但是模型的表現不是很穩定。

接下來的模型微調分為兩個步驟:

  • 使用 RandomizedSearchCV 尋找最佳引數的大致範圍
  • 使用 GridSearchCV 尋找更精確的引數

影響 Ada Boost 效能的引數大致如下:

  • n_estimators
  • base_estimator
  • learning_rate

注意,RandomizedSearchCV 和 GridSearchCV 都會使用交叉驗證來評估各個模型的表現。在前文中我們提到,時間序列是需要考慮順序的。對於已經經過轉換來適應機器學習模型的時間序列,每條記錄都有其相對應的時間資訊,訓練集中也沒有測試集的資訊。訓練集中記錄的順序可以按照特定的交叉驗證順序排列(較為複雜),也可以被打亂。這裡,我們認為訓練集資料被打亂不影響模型訓練。

base_estimator 是 ada boost 提升演算法的基礎,我們需要提前建立一個 base_estimator 的列表。

l_base_estimator = []
for i in range(1,16):
    base = DecisionTreeRegressor(max_depth=i, random_state=42)
    l_base_estimator.append(base)
l_base_estimator += [LinearSVR(random_state=42,epsilon=0.01,C=100)]

1.  使用 RandomizedSearchCV 尋找最佳引數的大致範圍

使用 RandomSearchCV,隨機嘗試引數。這裡,我們嘗試了500種不同的引數組合。

randomized_search_grid = {'n_estimators':[10, 50, 100, 500, 1000, 5000],
                          'base_estimator':l_base_estimator,
                          'learning_rate':np.linspace(0.01,1)}
search = RandomizedSearchCV(AdaBoostRegressor(random_state=42),
                            randomized_search_grid, 
                            n_iter=500, 
                            scoring='neg_mean_absolute_error', 
                            n_jobs=-1, 
                            cv=5, 
                            random_state=42)
result = search.fit(x_train, y_train)

可以看到,500種引數組合中表現最佳的是:

result.best_params_

result.best_score_

2.  使用 GridSearchCV 尋找更精確的引數

根據 Randomized Search 的結果,我們再使用 GridSearchCV 進行更深一步的微調:

  • n_estimators: 1-50
  • base_estimator: Decision Tree with max depth 9
  • learning_rate: 0.7左右
search_grid = {'n_estimators':range(1,51),
               'learning_rate':np.linspace(0.6,0.8,num=20)}

GridSearchCV 的結果如下:

根據 GridSearchCV 的結果,我們保留最佳模型,讓其在整個訓練集上訓練,並在測試集上進行預測,對結果進行評估。

可以看到,結合訓練集的交叉驗證結果,最佳模型在測試集中的表現與模型選擇和訓練階段的結果相比,準確度略有提升。最佳模型平衡了訓練集和測試集表現,可以更有效地防止過擬合的情況出現。

在確定模型以後,因為之前的模型都只接觸過訓練集,為了預測未來的資料,我們需要將模型在所有資料上重新訓練一遍,並以 pickle 檔案的格式儲存這個最佳模型。

best_reg.fit(x_mod, y)

該模型在全量資料的預測結果中MAPE值為:

m_forecast = best_reg.predict(x_mod)
mean_absolute_percentage_error(y, m_forecast)

模型預測

與傳統的 ARIMA 模型不同,現有模型的每次預測都需要將預測資訊重新整合,輸入進模型後才能得到新的預測結果。輸入資料的重新整合可以用以下方程進行開發,方便適應各種應用場景的需求。

def forecast_one_period(price_info_adj_data, ml_model, data_processor):
    # Source data: Data acquired straight from source
    last_record = price_info_adj_data.reset_index().iloc[-1,:]
    next_day = last_record['Date'] + relativedelta(days=1)
    next_day_t_1_PricePctDelta = last_record['AdjPricePctDelta']
    next_day_t_2_PricePctDelta = last_record['t_1_PricePctDelta']
    next_day_t_1_VolumeDelta = last_record['Volume_in_M'] - last_record['t_1_VolumeDelta']
    if next_day_t_1_PricePctDelta > 0:
        next_day_sign_t_1 = 1
    else:
        next_day_sign_t_1 = 0
    # Value -99999 is a placeholder which won't be used in the following modeling process
    next_day_input = (pd.DataFrame({'Date':[next_day], 
                                    'Volume_in_M':[-99999],
                                    'AdjPricePctDelta':[-99999], 
                                    't_1_PricePctDelta':[next_day_t_1_PricePctDelta],
                                    't_2_PricePctDelta':[next_day_t_2_PricePctDelta], 
                                    't-1volume': last_record['Volume_in_M'],
                                    't-2volume': last_record['t-1volume'],
                                    't_1_VolumeDelta':[next_day_t_1_VolumeDelta],
                                    'sign_t_1':next_day_sign_t_1}).set_index('Date'))
    # If forecast period is post Feb 15, 2020, input data starts from 2020-02-16, 
    # as our model is dedicated for market under Covid Impact.
    # Another model could be used for pre-Covid market forecast.
    if next_day > datetime.datetime(2020, 2, 15):
        price_info_adj_data = price_info_adj_data[price_info_adj_data.index > datetime.datetime(2020, 2, 15)]
    price_info_adj_data_next_day = pd.concat([price_info_adj_data, next_day_input])
    # Add new record to original data for modeling preparation
    input_modified = processor.data_modification(price_info_adj_data_next_day)
    # Prep for modeling
    x,y = data_processor.data_modeling_prep(input_modified)
    next_day_x = x.iloc[-1:]
    forecast_price_delta = ml_model.predict(next_day_x)
    # Consolidate prediction results
    forecast_df = {'Date':[next_day], 'price_pct_delta':[forecast_price_delta[0]], 'actual_pct_delta':[np.nan]}
    return pd.DataFrame(forecast_df)

我們讀取之前儲存的模型,對未來一個工作日的價格變動進行預測。輸出的結果中,actual_pct_delta 是為未來價格釋出後儲存真實結果所預留的結構。

根據預測結果,我們認為2022年11月1日這天標普指數會有輕微的上升。

分析預測結果

根據近兩年的資料走向,我們有了這樣的預測結果:標普指數會有輕微的上升。但當我們檢視2022年11月1日釋出的實際資料時發現,指數在當天是下降的。這意味著外界的某種資訊,可能是經濟指標抑或是政策風向的改變,導致市場情緒有所變化。搜尋相關新聞後,我們發現了以下資訊:

“Stocks finished lower as data showing a solid US labor market bolstered speculation that Federal Reserve policy could remain aggressively tight even with the threat of a recession.”

在經濟面臨多重考驗的同時,招聘市場職位數量上升的資訊釋出,導致投資者認為招聘市場表現穩健,美聯儲不會考慮放寬當下的經濟政策;這種負面的展望在股票市場上得到了呈現,導致當日指數收盤價下降。

模型在實際應用中不僅僅充當著預測的工作,在本文的案例中,指數價格變動的預測更類似於一種 “標線”。通過模型學習歷史資料,模型的結果代表著如果按照歷史記錄的資訊,沒有外部重大幹擾的情況下,我們所期待的變動大致是怎樣的,即當日實際發生的變動是“系統”層面的變動,還是需要深度挖掘的非“系統”因素所造成的變動。在模型的基礎上,我們可以將這些結果舉一反三,開發出各式各樣的功能,讓資料儘可能地發揮其價值。

總結

回顧上下兩篇文章的全部內容,標普500股票指數的價格預測思路總結如下:

  • 確定預測目標:反映北美股票市場的指數 — 標普500 ;
  • 資料收集:從公共金融網站下載歷史價格資料;
  • 探索性資料分析:初步瞭解資料的特性,資料視覺化,將時間序列資訊以影象的形式呈現;
  • 資料預處理:將時間轉換為變數,更改價格資料,尋找週期和季節性,根據週期調整交易量資料;
  • 資料工程:處理缺失值並提取所需變數,資料標準化,處理分類變數;
  • 模型選擇和訓練:拆分訓練集和測試集,確定模型方向和評估指標,嘗試訓練各種模型;
  • 模型評估:根據指標選定最優模型,使用 RandomizedSearchCV 尋找最佳引數的大致範圍,再使用 GridSearchCV 尋找更精確的引數;
  • 模型預測:整合輸入資料,預測未來一個工作日的價格變動;
  • 分析預測結果:結合當日的實際情況,理解市場變動,發揮模型價值。

 


參考資料: