XGBoost 6-model ensemble on 8 coins — 21 features, per-pair models
Timeframe
15m
Direction
Long & Short
Stoploss
-2.5%
Trailing Stop
Yes
ROI
0m: 6.0%, 480m: 4.0%, 1440m: 2.5%, 4320m: 0.0%
Interface Version
3
Startup Candles
3000
Indicators
8
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
"""ML on 8 mainstream coins — RandomForest, XGBoost, LightGBM"""
from pandas import DataFrame
import talib.abstract as ta
import numpy as np
from freqtrade.strategy import IStrategy
COINS = ["BTC/USDT:USDT","ETH/USDT:USDT","SOL/USDT:USDT","BNB/USDT:USDT",
"XRP/USDT:USDT","DOGE/USDT:USDT","ADA/USDT:USDT","AVAX/USDT:USDT"]
def feats(d):
df = d.copy()
for p in [1,3,6,12,24]: df['r'+str(p)] = df['close'].pct_change(p)
df['hlr'] = (df['high']-df['low'])/df['close']; df['cp'] = (df['close']-df['low'])/(df['high']-df['low']+1e-6)
for p in [9,21,55]: e = ta.EMA(df,p); df['e'+str(p)+'d'] = (df['close']-e)/e*100
md = ta.MACD(df); df['md'] = md['macd']; df['ms'] = md['macdsignal']; df['mh'] = md['macdhist']
df['rsi'] = ta.RSI(df,14); df['adx'] = ta.ADX(df,14)
bb = ta.BBANDS(df,20); df['bw']=(bb['upperband']-bb['lowerband'])/bb['middleband']; df['bbp']=(df['close']-bb['lowerband'])/(bb['upperband']-bb['lowerband']+1e-6)
df['atr'] = ta.ATR(df,14)/df['close']*100; df['vr'] = df['volume']/ta.SMA(df['volume'],20)
st = ta.STOCH(df); df['sk'] = st['slowk']; df['sd'] = st['slowd']
return df
COLS = ['r1','r3','r6','r12','r24','hlr','cp','e9d','e21d','e55d','md','ms','mh','rsi','adx','bw','bbp','atr','vr','sk','sd']
def get_xy(d):
d2 = feats(d); ret = d2['close'].pct_change(12).shift(-12).fillna(0)
X = np.nan_to_num(d2[COLS].values,0,0,0); y = np.where(ret>0.005,2,np.where(ret<-0.005,0,1))
return X, y
class ML8_XGB(IStrategy):
"""XGBoost 6-model ensemble on 8 coins — 21 features, per-pair models"""
INTERFACE_VERSION = 3; timeframe = '15m'; can_short = True
stoploss = -0.025; trailing_stop = True
trailing_stop_positive = 0.005; trailing_stop_positive_offset = 0.018
trailing_only_offset_is_reached = True
minimal_roi = {"0":0.06,"480":0.04,"1440":0.025,"4320":0}
max_open_trades = 4; startup_candle_count = 3000
process_only_new_candles = False; use_exit_signal = False
def __init__(self, config):
super().__init__(config); self._m = {}; self._sm = {}; self._ss = {}
def populate_indicators(self, dataframe, metadata):
pair = metadata['pair']; n = len(dataframe)
if pair not in self._m and n >= 2200:
self._train(dataframe.iloc[:2000].copy(), pair)
if pair in self._m:
d2 = feats(dataframe)
X2 = np.nan_to_num(d2[COLS].values,0,0,0)
Xs = (X2-self._sm[pair])/self._ss[pair]
ap = np.mean([m.predict_proba(Xs) for m in self._m[pair]],axis=0)
cl = self._m[pair][0].classes_; li=list(cl).index(2); si=list(cl).index(0)
dataframe['xs']=ap[:,li]-ap[:,si]; dataframe['xc']=np.maximum(ap[:,li],ap[:,si])
return dataframe
def _train(self, df, pair):
import xgboost as xgb; X,y = get_xy(df)
v=np.arange(len(X)-12); v2=np.array([i for i in v if not np.isnan(y[i]) and not np.isnan(X[i]).any()])
Xt,yt = X[v2],y[v2]
if len(Xt)<400: return False
self._sm[pair]=Xt.mean(0); self._ss[pair]=Xt.std(0)+1e-6; Xs=(Xt-self._sm[pair])/self._ss[pair]
u,c=np.unique(yt,return_counts=True); w={u[i]:len(yt)/(len(u)*c[i]) for i in range(len(u))}; sw=np.array([w[z] for z in yt])
ms=[]
for s in [42,73,99,17,55,27]:
m=xgb.XGBClassifier(n_estimators=250,max_depth=4,learning_rate=0.04,subsample=0.8,colsample_bytree=0.7,min_child_weight=3,reg_alpha=0.2,reg_lambda=1.0,random_state=s,verbosity=0)
m.fit(Xs,yt,sample_weight=sw); ms.append(m)
self._m[pair]=ms; return True
def populate_entry_trend(self, d, m):
if 'xs' not in d.columns: return d
d.loc[(d['xc']>0.55)&(d['xs']>0),['enter_long','enter_tag']]=(1,'L')
d.loc[(d['xc']>0.55)&(d['xs']<0),['enter_short','enter_tag']]=(1,'S')
return d
def populate_exit_trend(self, d, m): return d
class ML8_RF(IStrategy):
"""Random Forest on 8 coins — faster training, less overfit"""
INTERFACE_VERSION = 3; timeframe = '15m'; can_short = True
stoploss = -0.025; trailing_stop = True
trailing_stop_positive = 0.005; trailing_stop_positive_offset = 0.018
trailing_only_offset_is_reached = True
minimal_roi = {"0":0.06,"480":0.04,"1440":0.025,"4320":0}
max_open_trades = 4; startup_candle_count = 3000
process_only_new_candles = False; use_exit_signal = False
def __init__(self, config):
super().__init__(config); self._m = {}; self._sm = {}; self._ss = {}
def populate_indicators(self, dataframe, metadata):
pair = metadata['pair']; n = len(dataframe)
if pair not in self._m and n >= 2200:
self._train(dataframe.iloc[:2000].copy(), pair)
if pair in self._m:
d2 = feats(dataframe)
X2 = np.nan_to_num(d2[COLS].values,0,0,0)
Xs = (X2-self._sm[pair])/self._ss[pair]
p = self._m[pair].predict_proba(Xs)
cl = self._m[pair].classes_; li=list(cl).index(2); si=list(cl).index(0)
dataframe['xs']=p[:,li]-p[:,si]; dataframe['xc']=np.maximum(p[:,li],p[:,si])
return dataframe
def _train(self, df, pair):
from sklearn.ensemble import RandomForestClassifier
X,y = get_xy(df)
v=np.arange(len(X)-12); v2=np.array([i for i in v if not np.isnan(y[i]) and not np.isnan(X[i]).any()])
Xt,yt = X[v2],y[v2]
if len(Xt)<400: return False
self._sm[pair]=Xt.mean(0); self._ss[pair]=Xt.std(0)+1e-6
self._m[pair]=RandomForestClassifier(n_estimators=150,max_depth=8,min_samples_leaf=15,random_state=42,n_jobs=-1)
self._m[pair].fit((Xt-self._sm[pair])/self._ss[pair],yt); return True
def populate_entry_trend(self, d, m):
if 'xs' not in d.columns: return d
d.loc[(d['xc']>0.55)&(d['xs']>0),['enter_long','enter_tag']]=(1,'L')
d.loc[(d['xc']>0.55)&(d['xs']<0),['enter_short','enter_tag']]=(1,'S')
return d
def populate_exit_trend(self, d, m): return d