關于機器學習與比特幣的示例
我曾經編寫過豬老三的世界中使用機器學習對股價和漲跌進行預測的幻想示例:
在豬老三的世界中實現了:機器學習.fit(x, y) = (股價預測,漲跌預測) =發財
在豬老三的童話中我設定的可以影響股價走勢的參數是有限個的,特別是影響漲跌的因素很容易構造特征。在真實的市場中可以影響股價走勢的因素是***多的,而且這些因素之間的也可以是相關的。就像你求解一個方程組,這個方程組不是有一個兩個解,它是有***個解的系統,并且每個解都與其他任意個解相關,但又并非簡單線性相關。市場是一個二級混沌系統,認為任何想通過技術對股價進行預測或者漲跌預測都是不可能的,不倫你自己認為你使用的技術本身有多高深,無異于管中窺豹。
本系列教程中講到的ump裁判系統是abupy中通過機器學習技術對回測結果進行學習,反向指導決策新的交易是否攔截的實際應用,本節講解機器學習在量化領域很有作用的另一個方向:閥值的估計,因為無論是編寫選股策略,擇時策略還是任何涉及決策的代碼模型中都離不開閥值,比如最常用的止盈止損策略,在代碼的編寫中就一定會涉及閥值,比如之前的章節一直使用的abupy中內置止盈止損策略AbuFactorCloseAtrNStop。
之所以一定會涉及閥值的確定是因為就像剛剛說的類似你求解一個方程組,如果所有的參數都是未知數,那么你怎么解出你需要的答案,所以一定要把一些變量變成常數值,然后通過這些常數值來確定更多的變量,最終解出你所關心的解。
對于閥值的確定傳統的做法是根據經驗來設定,實際上所謂的經驗是個體對問題的統計模型, 在機器學習技術的幫助下,可以實現更客觀,全面,適應范圍更廣的閥值設定。
不論是個體經驗對閥值進行常量定估還是通過機器學習技術通過數據模型對閥值進行定估,都達不到絕對準確預測結果的目標,量化交易的概率優勢并不具有絕對的優勢,即達不到預測的程度,量化交易中大多策略是基于對歷史規律的總結,在規律的基礎上發現概率優勢,它的***理論依據是人性的相似性以及人性很難改變的事實,如果每一個瞬間的股票價格都是全體交易者對價值所達成的一種瞬間共識,那么歷史的規律在今后的交易中同樣具有指導意義。
按照上面解方程的說法就是,定估的常量只要能滿足大多數時候解出合理的解,甚至很多時候定估的常量只要能滿足有時候能解出合理的解就可以,對于有時候能解出合理的情況,可以在上層通過非均衡技術對決策進行二次邏輯過濾,我反復提及過的非均衡技術思想是量化中很很重要的一種設計思路,因為我們量化的目標結果就是非均衡,我們想要贏的錢比輸的多。
1. 比特幣特征的提取
下面通過對比特幣的短線交易決策為例,示例上述論點以及abupy中機器學習模塊的使用,以及數據獲取:
- from abupy import abu, ml, nd, tl, pd_resample, AbuML, AbuMLPd, AbuMetricsBase
- from abupy import ABuSymbolPd, ABuScalerUtil, get_price, ABuMarketDrawing, ABuKLUtil
- # btc是比特幣symbol代號
- btc = ABuSymbolPd.make_kl_df('btc', start='2013-09-01', end='2017-07-26')
之前在比特幣, 萊特幣的回測那節使用ABuKLUtil.date_week_wave對走勢的日震蕩做過統計如下:
- ABuKLUtil.date_week_wave(btc)
- date_week
- 周一 5.0108
- 周二 5.5610
- 周三 5.4437
- 周四 5.7275
- 周五 5.3008
- 周六 4.7875
- 周日 4.6528
- Name: wave, dtype: float64
從上面可以看出大概0.055的日震蕩幅度可以成做大波動的交易對比特幣來說,下面對數據添加新列big_wave,可以看到結果大波動的占總交易日的1/3:
- btc['big_wave'] = (btc.high - btc.low) / btc.pre_close > 0.055
- btc['big_wave'] = btc['big_wave'].astype(int)
- btc['big_wave'].value_counts()
- 0 1005
- 1 420
- Name: big_wave, dtype: int64
任何大的決策其實都是由很多看極起來極不起眼的小事組成的,如果我們是做比特幣日內的交易者,首先你需要判斷今天適不適合做交易,做出這個判斷的依據里有一條即是今天的波動需要足夠大,下面通過機器學習技術演示如何決策今天的波動是否足夠大。
備注:由于abupy中內置沙盒數據沒有分時的數據,所以本示例使用日線數據做為分析對象,實際策略中應該使用的是分鐘數據
首先切割訓練集和測試集,保留***60天走勢數據做為測試集數據:
- btc_train_raw = btc[:-60]
- btc_test_raw = btc[-60:]
下面為訓練集和測試集數據都加上5,10,21,60日均線特征:
- def calc_ma(tc, ma):
- ma_key = 'ma{}'.format(ma)
- tc[ma_key] = nd.ma.calc_ma_from_prices(tc.close, ma, min_periods=1)
- for ma in [5, 10, 21, 60]:
- calc_ma(btc_train_raw, ma)
- calc_ma(btc_test_raw, ma)
- btc_train_raw.tail(1)
編寫特征抽取組合函數btc_siblings_df:
- 它首先將所有交易日以3個為一組,切割成多個子df,即每一個子df中有3個交易日的交易數據
- 使用數據標準化將連續3天交易日中的連續數值特征進行標準化操作
- 抽取***天,第二天的大多數特征分別改名字以one,two為特征前綴,如:one_open,one_close,two_ma5,two_high…..,
- 第三天的特征只使用’open’, ‘low’, ‘pre_close’, ‘date_week’,該名前綴today,如today_open,today_date_week
- 第三天的抽取了’big_wave’,其將在之后做為y
- 將抽取改名字后的特征連接起來組合成為一條新數據,即3天的交易數據特征->1條新的數據
代碼如下所示:
- def btc_siblings_df(btc_raw):
- # 將所有交易日以3個為一組,切割成多個子df,即每一個子df中有3個交易日的交易數據
- btc_siblings = [btc_raw.ix[sib_ind * 3:(sib_ind + 1) * 3, :]
- for sib_ind in np.arange(0, int(btc_raw.shape[0] / 3))]
- btc_df = pd.DataFrame()
- for sib_btc in btc_siblings:
- # 使用數據標準化將連續3天交易日中的連續數值特征進行標準化操作
- sib_btc_scale = ABuScalerUtil.scaler_std(
- sib_btc.filter(['open', 'close', 'high', 'low', 'volume', 'pre_close',
- 'ma5', 'ma10', 'ma21', 'ma60', 'atr21', 'atr14']))
- # 把標準化后的和big_wave,date_week連接起來
- sib_btc_scale = pd.concat([sib_btc['big_wave'], sib_btc_scale, sib_btc['date_week']], axis=1)
- # 抽取***天,第二天的大多數特征分別改名字以one,two為特征前綴,如:one_open,one_close,two_ma5,two_high.....
- a0 = sib_btc_scale.iloc[0].filter(['open', 'close', 'high', 'low', 'volume', 'pre_close',
- 'ma5', 'ma10', 'ma21', 'ma60', 'atr21', 'atr14', 'date_week'])
- a0.rename(index={'open': 'one_open', 'close': 'one_close', 'high': 'one_high', 'low': 'one_low',
- 'volume': 'one_volume', 'pre_close': 'one_pre_close',
- 'ma5': 'one_ma5', 'ma10': 'one_ma10', 'ma21': 'one_ma21',
- 'ma60': 'one_ma60', 'atr21': 'one_atr21', 'atr14': 'one_atr14',
- 'date_week': 'one_date_week'}, inplace=True)
- a1 = sib_btc_scale.iloc[1].filter(['open', 'close', 'high', 'low', 'volume', 'pre_close',
- 'ma5', 'ma10', 'ma21', 'ma60', 'atr21', 'atr14', 'date_week'])
- a1.rename(index={'open': 'two_open', 'close': 'two_close', 'high': 'two_high', 'low': 'two_low',
- 'volume': 'two_volume', 'pre_close': 'two_pre_close',
- 'ma5': 'two_ma5', 'ma10': 'two_ma10', 'ma21': 'two_ma21',
- 'ma60': 'two_ma60', 'atr21': 'two_atr21', 'atr14': 'two_atr14',
- 'date_week': 'two_date_week'}, inplace=True)
- # 第三天的特征只使用'open', 'low', 'pre_close', 'date_week',該名前綴today,如today_open,today_date_week
- a2 = sib_btc_scale.iloc[2].filter(['big_wave', 'open', 'low', 'pre_close', 'date_week'])
- a2.rename(index={'open': 'today_open', 'low': 'today_low',
- 'pre_close': 'today_pre_close',
- 'date_week': 'today_date_week'}, inplace=True)
- # 將抽取改名字后的特征連接起來組合成為一條新數據,即3天的交易數據特征->1條新的數據
- btc_df = btc_df.append(pd.concat([a0, a1, a2], axis=0), ignore_index=True)
- return btc_df
第三天的特征避免使用high是因為訓練的目標y是big_wave,之所以可以使用當天的low是因為比特幣市場的特點為24小時交易,即沒有明確的一天的概念,也即沒有明確一天中的***,實盤使用即人工對當前***根據24小時***進行猜測,或直接使用24小時***,或者使用當前的***價格都可,組成數據后使用模型進行決策,決策的結果即為未來數小時內是否會有大的波動,實際上最終大波動的決策成立,需要其它很多模型共同生效,比如外盤的走勢決策等等。
下面使用訓練集數據btc_train_raw做為參數抽取組合特征,重新組合好的特征如tail所示:
- btc_train0 = btc_siblings_df(btc_train_raw)
- btc_train0.tail()

如上所示這樣我們只能得到454條訓練集數據,但由于每3條連續交易日數據組合成一個特征,只要向前跳一條數據進行特征組合抽取即可以得到另一組新特征,下面分別跳過***個,第二個數據,抽取btc_train1,btc_train2:
- btc_train1 = btc_siblings_df(btc_train_raw[1:])
- btc_train2 = btc_siblings_df(btc_train_raw[2:])
下面把上面的3組特征連起來,然后把周幾這個特征使用pd.get_dummies進行離散化處理,使得所有特征值的范圍都在0-1之間,最終的特征如下btc_train所示:
- btc_train = pd.concat([btc_train0, btc_train1, btc_train2])
- btc_train.index = np.arange(0, btc_train.shape[0])
- dummies_one_week = pd.get_dummies(btc_train['one_date_week'], prefix='one_date_week')
- dummies_two_week = pd.get_dummies(btc_train['two_date_week'], prefix='two_date_week')
- dummies_today_week = pd.get_dummies(btc_train['today_date_week'], prefix='today_date_week')
- btc_train.drop(['one_date_week', 'two_date_week', 'today_date_week'], inplace=True, axis=1)
- btc_train = pd.concat([btc_train, dummies_one_week, dummies_two_week, dummies_today_week], axis=1)
- pd.options.display.max_rows=10
- btc_train
2. abu中內置機器學習模塊的使用
下面使用abupy中內置機器學習工具AbuML對特征數據進行封裝,代碼如下所示,下面的y值即是big_wave列:
- train_matrix = btc_train.as_matrix()
- y = train_matrix[:, 0]
- x = train_matrix[:, 1:]
- btc_ml = AbuML(x, y, btc_train)
AbuML會根據數據特點自動內部選擇使用分類器或者回歸器,下面進行特指***分類器操作,比如特指使用隨機森林做為分類器,執行random_forest_classifier_best會在內部對n_estimators參數和訓練集數據進行grid search擬合,尋找最合適的參數最終做為內部分類器:
- btc_ml.random_forest_classifier_best()
- start grid search please wait...
- RandomForestClassifier(bootstrap=True, class_weight=None, criterion='gini',
- max_depth=None, max_features='auto', max_leaf_nodes=None,
- min_impurity_split=1e-07, min_samples_leaf=1,
- min_samples_split=2, min_weight_fraction_leaf=0.0,
- n_estimators=260, n_jobs=1, oob_score=False, random_state=None,
- verbose=0, warm_start=False)
所有best函數中尋找***參數內部只根據學習器的特點尋找最少量的參數設置,如果需要自定義參數范圍可使用如下所示:
- param_grid = {'max_features': ['sqrt', 'log2'], 'n_estimators': np.arange(50, 500, 50)}
- btc_ml.random_forest_classifier_best(param_grid=param_grid)
- start grid search please wait...
- RandomForestClassifier(bootstrap=True, class_weight=None, criterion='gini',
- max_depth=None, max_features='sqrt', max_leaf_nodes=None,
- min_impurity_split=1e-07, min_samples_leaf=1,
- min_samples_split=2, min_weight_fraction_leaf=0.0,
- n_estimators=400, n_jobs=1, oob_score=False, random_state=None,
- verbose=0, warm_start=False)
下面使用btc_ml對訓練集進行交叉準確率評分:
- btc_ml.cross_val_accuracy_score()
- RandomForestClassifier score mean: 0.8151620867325032
- array([ 0.781 , 0.8102, 0.7883, 0.8382, 0.8162, 0.8162, 0.8235,
- 0.8456, 0.7794, 0.8529])
下面使用btc_ml對訓練集進行交叉roc_auc評分:
- btc_ml.cross_val_roc_auc_score()
- RandomForestClassifier score mean: 0.8399573797130188
- array([ 0.815 , 0.8785, 0.8166, 0.8018, 0.8707, 0.8484, 0.8148,
- 0.8551, 0.8005, 0.8981])
AbuML對外的函數都支持關鍵子參數fiter_type,可以指定使用的學習器類型如回歸,聚類等,每個函數都通過內部裝飾器聲明自己支持的學習器類型對不支持的類型輸出不支持,如下想通過指定使用回歸器進行roc_auc評分:
- btc_ml.cross_val_roc_auc_score(fiter_type=ml.EMLFitType.E_FIT_REG)
- cross_val_roc_auc_score not support reg!
上述輸出顯示函數不支持回歸器,因為內部實現中通過entry_wrapper裝飾器聲明了只支持E_FIT_CLF,即分類器:
- @entry_wrapper(support=(EMLFitType.E_FIT_CLF,))
- def cross_val_roc_auc_score(self, cv=10, **kwargs):
- """
- 被裝飾器entry_wrapper(support=(EMLFitType.E_FIT_CLF,))裝飾,
- 即支持有監督學習分類,使用cross_val_score對數據進行roc_auc度量,如果數據的y的
- label標簽 > 2,通過label_binarize將label標簽進行二值化處理,
- 依次計算二值化的列的roc_auc,結果返回score***的數據度量
- :param cv: 透傳cross_val_score的參數,默認10
- :param kwargs: 外部可以傳遞x, y, 通過
- x = kwargs.pop('x', self.x)
- y = kwargs.pop('y', self.y)
- 確定傳遞self._do_cross_val_score中參數x,y,
- 以及裝飾器使用的fiter_type,eg:
- ttn_abu.cross_val_roc_auc_score(fiter_type=ml.EMLFitType.E_FIT_REG)
- :return: cross_val_score返回的score序列,
- eg: array([ 1. , 0.9 , 1. , 0.9 , 1. , 0.9 , 1. , 0.9 , 0.95, 1. ])
- """
- x = kwargs.pop('x', self.x)
- y = kwargs.pop('y', self.y)
- return self._do_cross_val_score(x, y, cv, _EMLScoreType.E_SCORE_ROC_AUC.value)
更多詳情實現請閱讀源代碼AbuML
下面使用train_test_split_xy查看訓練集輸出的precision_score,recall_score,混淆矩陣,以及f1等度量結果:
- btc_ml.train_test_split_xy()
- x-y:(1363, 48)-(1363,)
- train_x-train_y:(1226, 48)-(1226,)
- test_x-test_y:(137, 48)-(137,)
- accuracy = 0.77
- precision_score = 0.62
- recall_score = 0.39
- Predicted
- | 0 | 1 |
- |-----|-----|
- 0 | 90 | 9 |
- Actual |-----|-----|
- 1 | 23 | 15 |
- |-----|-----|
- precision recall f1-score support
- 0.0 0.80 0.91 0.85 99
- 1.0 0.62 0.39 0.48 38
- avg / total 0.75 0.77 0.75 137
下面通過plot_roc_estimator繪制roc曲線:
- btc_ml.plot_roc_estimator()
下面通過plot_confusion_matrices繪制混淆矩陣:
- btc_ml.plot_confusion_matrices()
- [[915 65]
- [183 200]]
下面通過plot_decision_function繪制決策邊界,由于繪制2d圖,內部已經使用pca將數據特征降維后再繪制:
- btc_ml.plot_decision_function()
下面通過plot_graphviz_tree繪制邏輯決策圖:
- btc_ml.plot_graphviz_tree()
- RandomForestClassifier not hasattr tree_, use decision tree replace
下面通過feature_selection對特征的支持度進行評級:
- pd.options.display.max_rows = 48
- btc_ml.feature_selection(show=False).sort_values(by='ranking')
下面通過importances_coef_pd對特征的重要程度進行量化:
- btc_ml.importances_coef_pd().sort_values(by='importance')[::-1]
3. 測試集的驗證與非均衡技術
下面將前面保留切割的60條測試數據進行特征抽取組合,方式和抽取訓練集時一樣,代碼如下所示:
- btc_test0 = btc_siblings_df(btc_test_raw)
- btc_test1 = btc_siblings_df(btc_test_raw[1:])
- btc_test2 = btc_siblings_df(btc_test_raw[2:])
- btc_test = pd.concat([btc_test0, btc_test1, btc_test2])
- btc_test.index = np.arange(0, btc_test.shape[0])
- dummies_one_week = pd.get_dummies(btc_test['one_date_week'], prefix='one_date_week')
- dummies_two_week = pd.get_dummies(btc_test['two_date_week'], prefix='two_date_week')
- dummies_today_week = pd.get_dummies(btc_test['today_date_week'], prefix='today_date_week')
- btc_test.drop(['one_date_week', 'two_date_week', 'today_date_week'], inplace=True, axis=1)
- btc_test = pd.concat([btc_test, dummies_one_week, dummies_two_week, dummies_today_week], axis=1)
- matrix_test = btc_test.as_matrix()
- y_test = matrix_test[:, 0]
- x_test = matrix_test[:, 1:]
對測試集數據進行準確率評估,代碼如下所示:
- from sklearn import metrics
- y_predict = [btc_ml.predict(x_test[test_ind])[0] for test_ind in np.arange(0, matrix_test.shape[0])]
- print('測試集正確率{:3f}'.format(metrics.accuracy_score(y_test, y_predict)))
- 測試集正確率0.620690
如上所示上面的準確率結果為60%以上正確,下面使用predict_proba看看概率結果:
- y_prob = [btc_ml.predict_proba(x_test[test_ind])[0] for test_ind in np.arange(0, matrix_test.shape[0])]
- y_prob[-10:]
- [array([ 0.495, 0.505]),
- array([ 0.9075, 0.0925]),
- array([ 0.7875, 0.2125]),
- array([ 0.83, 0.17]),
- array([ 0.8375, 0.1625]),
- array([ 0.96, 0.04]),
- array([ 0.58, 0.42]),
- array([ 0.495, 0.505]),
- array([ 0.6575, 0.3425]),
- array([ 0.565, 0.435])]
本節開始的時候說過很多時候定估的決策只要能滿足有時候能解出合理的解就可以,通過非均衡技術對決策進行二次邏輯過濾即可,下面解釋這句話的意思,通過上面y_predict輸出你可以看到準確率結果為60%以上,如果你只按照這個決策認定比特幣今天是否有大行情還是有很多錯誤的可能,上面的y_prob輸出了每一個決策的概率。
那么我們如果希望做比特幣交易只希望在決策模型有很大把握的情況下才做,否則寧可少賺點錢也不做應該怎么做呢?
上面predict的決策在某一個值在0.5以上即進行了判斷,如果我們希望把握更大一些就需要調整這個值,比如0.55以上才能認定決策,那首要的問題就是如何選定這個閥值,下面示例使用search_match_pos_threshold進行閥值的確定:
- btc_ml.search_match_pos_threshold(accuracy_match=0.85)
- 0.580 satisfy require, accuracy:0.854, effect_rate:0.879
結果如上所示,search_match_pos_threshold函數的作用是對訓練集數據進行非均衡結果度量,從0.5至0.99的范圍內開始不斷向上調整決策閥值:
比如測試閥值為0.55,那么如果決策的概率為array([ 0.5378, 0.4622]),那么通過閥值二分化后結果為(0, 0),這個結果將不計入正確率統計中,即認為未達成有效的決策,但如果決策的概率為 array([ 0.9711, 0.0289]),那么通過閥值二分化后結果為(1, 0),認為達成有效的決策,當有效的決策正確率達到參數accuracy_match傳遞的值,本例中使用0.85即85%的正確率時,停止搜索,本例返回的結果為0.580,即使用閥值0.580可以達成非均衡決策的85%正確率。
備注:與之對應search_match_neg_threshold進行閥值搜索,從0.5至0.01的范圍內開始不斷向下調整決策閥值,更多詳情請閱讀源代碼。
下面使用0.580做為閥值,通過predict_proba_threshold函數進行決策,代碼如下:
- y_prob_threshold = [btc_ml.predict_proba_threshold(x_test[test_ind], 0.580, 0)
- for test_ind in np.arange(0, matrix_test.shape[0])]
上面predict_proba_threshold傳遞的第三個參數0為未達成有效決策的情況下返回的決策結果,即閥值二分化后結果為(0, 0)后這里返回0,因為在比特幣這個示例中如果交易者想要保守的方式決策今天是否適合做日內交易,那么就希望只在有很大概率的情況下返回1,即適合交易,其它情況下沒有太大把握下全部返回0即可,下面使用precision_score計算查準率:
- metrics.precision_score(y_test, y_prob_threshold)
- 1.0
如上所示查準率100%正確,但召回率評分非常低,即比特幣在很多有大行情的情況下為了保守,放棄了日交易,只在把握大的時候行動:
- metrics.recall_score(y_test, y_prob_threshold)
- 0.20000000000000001
與上面的情況相反也有些激進的交易者想要的決策結果是只要今天不是很大把握說沒有行情,那就進行交易。
下面使用0.90做為閥值,通過predict_proba_threshold函數進行決策,第三個參數1即在為未達成有效決策的情況下返回的決策結果為1,可以看到結果的決策中大多數都被決策為1,代碼如下:
- y_prob_threshold = [btc_ml.predict_proba_threshold(x_test[test_ind], 0.90, 1)
- for test_ind in np.arange(0, matrix_test.shape[0])]
- pd.Series(y_prob_threshold).value_counts()
- 1 49
- 0 9
- dtype: int64
4. 繼承AbuMLPd對數據處理進行封裝
在abupy中不建議直接使用AbuML類進行構造,推薦使用繼承AbuMLPd后實現make_xy方法,在make_xy中將數據訓練集和測試集進行組裝完成,AbuMLPd基類通過代理方法對AbuML中的所有方法進行代理,即可以和使用AbuML中的方法一樣的接口操作,本節使用的示例內置在abupy項目中BtcBigWaveClf類,具體實現請直接閱讀BtcBigWaveClf。
小結:由于abupy中內置沙盒數據沒有分時的數據,所以本示例使用日線數據做為分析對象,實際策略中應該使用的是分鐘數據,本節示例主要為配合abupy中機器學習模塊的使用示例所寫,與真實的日內交易決策還有很大差別。