Сигналы на покупку и продажу в биржевой торговле

Пример с ежедневными данными по сырой нефти

Сигналы на покупку и продажу могут генерироваться двумя скользящими средними — длиннопериодной и короткопериодной. Когда короткая скользящая средняя поднимается или опускается ниже длинной скользящей средней, сигналы на покупку или продажу могут генерироваться на основе заданных параметров.

Здесь на простом примере мы показали, как формировать отчет по сигналам на покупку/продажу и визуализировать график.

Функция для извлечения данных:

def dataExtract(startDate,outputFile):
try:
data = pd.read_pickle(outputFile)
print(‘File data found…reading CrudeOil data’)
except FileNotFoundError:
print(‘File not found…downloading CrudeOil data’)
data = web.DataReader(‘CL=F’, ‘yahoo’, startDate)
data.to_pickle(outputFile)
return data

data = dataExtract(startDate=’2010-01-01′,

outputFile=’CrudeOilPrice.pkl’)
print(data.head())

Давайте сгенерируем сигналы на покупку/продажу, реализуя стратегию пересечения коротких скользящих средних (20 дней) и длинных скользящих средних (100 дней). В приведенном ниже коде можно заметить, что diff() применяется для ограничения сигнала на покупку/продажу.

Сигналы пересечения скользящих средних:

def MACrossOver(data, shortWindow, longWindow):
signals = pd.DataFrame(index=data.index)
signals[‘tradeSignal’] = 0.0
signals[‘MA20’] = data[‘Close’].rolling(window=shortWindow,
min_periods=1, center=False).mean()
signals[‘MA100’] = data[‘Close’].rolling(window=longWindow,
min_periods=1, center=False).mean()
signals[‘tradeSignal’][shortWindow:] = np.where(signals[‘MA20’][shortWindow:] > signals[‘MA100’][shortWindow:], 1.0, 0.0)
signals[‘finalSignal’] = signals[‘tradeSignal’].diff()
return signalsnewSeries = newSeries[‘2020-01-01’:]
DATA = data[‘2020-01-01’:]
fig = plt.figure(figsize= (15,6))
ax1 = fig.add_subplot(111, ylabel=’CrudeOil price in $’)
DATA[«Adj Close»].plot(ax=ax1, lw=.6)
newSeries[«MA20»].plot(ax=ax1, lw=2.)
newSeries[«MA100»].plot(ax=ax1, lw=2.)
ax1.plot(newSeries.loc[newSeries.finalSignal== 1.0].index, DATA[«Adj Close»][newSeries.finalSignal == 1.0],’^’, markersize=7, color=’green’)
ax1.plot(newSeries.loc[newSeries.finalSignal== -1.0].index, DATA[«Adj Close»][newSeries.finalSignal == -1.0],’v’, markersize=7, color=’red’)
plt.legend([«Adj Close Price»,»Short mavg»,»Long mavg»,»BuySignal»,»SellSignal»])
plt.title(«Simple Moving Average Crossover Strategy»)
plt.show()

Экспоненциальная скользящая средняя:

EMA придает больший вес недавним ценовым наблюдениям и меньший вес более старым ценовым наблюдениям. Эта интуиция заключается в том, что новое наблюдение за ценами имеет более актуальную информацию, чем предыдущее. Мы внедрим EMA с 20 днями и 10 днями в качестве количества периодов времени для вычисления среднего значения.

Давайте сосредоточимся на цене закрытия дневной акции.data[’ema5′] = (data[‘Adj Close’].ewm(span=5,adjust=True,ignore_na=True).mean())
data[’ema20′] = (data[‘Adj Close’].ewm(span=20,adjust=True,ignore_na=True).mean())
data.dropna(inplace=True)
t = data[‘2020-01-01’:].copy()
fig = go.Figure(data=[go.Candlestick(x=t.index,
open=t[‘Open’],
high=t[‘High’],
low=t[‘Low’],
close=t[‘Adj Close’])])
fig.add_trace(go.Scatter(x = t.index, y = t.ema5, marker = dict(
color = «blue»), name = «EMA5»))
fig.add_trace(go.Scatter(x = t.index, y = t.ema20, marker = dict(
color = «gray»), name = «EMA20»))
fig.update_xaxes(showline=True, linewidth=2, linecolor=’black’, mirror=True)
fig.update_yaxes(showline=True, linewidth=2, linecolor=’black’, mirror=True)
fig.update_layout(autosize = False, width = 1200, height = 600,)
fig.update_layout(title=’Candlestick plot::Crude oil prices’, yaxis_title='(US$)’)
fig.show()

Здесь мы видим, что ema5 более реактивна к новым наблюдениям, а ema20 менее реактивна к новым наблюдениям с более медленными затухающими свойствами. Фактически, более короткий период времени генерирует линию, близко следующую за фактическими свечами на графике. Визуально это означает, что существует больше случаев движения цены выше или ниже EMA, что можно интерпретировать как сигнал к открытию или закрытию позиции.

Функция для создания классификационных признаков:

def classificationFeatures(data):
data[‘CloseR’] = data[‘Adj Close’].pct_change()
data[‘LowR’] = data.Low.pct_change()
data[‘HighR’] = data.High.pct_change()
data[‘VolumeR’] = data.Volume.pct_change()
data[‘dailyChange’] = (data[‘Adj Close’] — data[‘Open’]) / data[‘Open’]
# data[’ema10′] = data[’ema10′]
# data[’ema20′] = data.ema20
data[‘oc’] = (data[‘Open’] — data[‘Adj Close’])
data[‘hl’] = (data[‘High’] — data[‘Low’])
data[‘target’] = np.where(data[‘Adj Close’].shift(-1) > data[‘Adj Close’], 1, 0)
data = data.replace([np.inf, -np.inf], np.nan)
data = data.dropna()
return data

data = classificationFeatures(data)
# X = preprocessing.scale(X)
storeDf = data[[‘Open’, ‘High’, ‘Low’, ‘Adj Close’, ‘Volume’]]

В приведенном выше коде целевая переменная равна 1, если цена закрытия завтра >, чем цена закрытия сегодня, и 0, если цена закрытия завтра <, чем цена закрытия сегодня.

Особенности X, Y:

def features():
X = data[[‘oc’, ‘hl’, ‘LowR’, ‘HighR’, ‘VolumeR’]]
Y = data.target
return X,Y
X,Y = features()

Разделение временных рядов на обучение/тестирование:

gkcv = GapKFold(n_splits=5, gap_before=2, gap_after=1)
# tscv = TimeSeriesSplit(n_splits=3)
for train_index, test_index in gkcv.split(X):
#print(«TRAIN:», train_index, «TEST:», test_index)
X_train, X_test = X.values[train_index], X.values[test_index]
y_train, y_test = Y[train_index], Y[test_index]

print(‘Length train set: {}’.format(len(y_train)))
print(‘Length test set: {}’.format(len(y_test)))

Классификация случайных лесов:

RF = RandomForestClassifier(bootstrap=True,
ccp_alpha=0.0,
class_weight=»balanced»,
max_leaf_nodes=None,
min_impurity_decrease=0.0,
min_impurity_split=None,
min_samples_leaf=1,
min_samples_split=2,
min_weight_fraction_leaf=0.0,
n_estimators=1000,
random_state=0,
verbose=0,
warm_start=False).fit(X,Y)
Score = cross_val_score(RF, X, Y, cv=gkcv).mean()
Score

Перекрестная проверка оценки в 51,73%, что не является хорошей оценкой, но удовлетворяет варианту использования классификационного моделирования. Чтобы повысить производительность, можно добавить различный набор функций и провести различный набор экспериментов, которые здесь не являются объективными.

Предсказание:

storeDf[‘PredictedSignal’] = (RF.predict(X))
storeDf[‘finalSignal’] = storeDf[‘PredictedSignal’].diff()
print(storeDf)
print(‘Number of trades (buy) = ‘, (storeDf[‘PredictedSignal’]==1).sum())
print(‘Number of trades (sell) = ‘, (storeDf[‘PredictedSignal’]==0).sum())
print()
print(‘Number of trades (buy) = ‘, (storeDf[‘finalSignal’]==1).sum())
print(‘Number of trades (sell) = ‘, (storeDf[‘finalSignal’]==-1).sum())

Визуализация:

%matplotlib inline

plotDf = storeDf[‘2021-01-01’:]
print(plotDf.finalSignal.value_counts())# Buy/Sell signals plot
buys = plotDf.loc[plotDf[«finalSignal»] == 1];
sells = plotDf.loc[plotDf[«finalSignal»] == -1];
# Plot
fig = plt.figure(figsize=(20, 5));
plt.plot(plotDf.index, plotDf[‘Adj Close’], lw=2., label=’Price’);
# Plot buy and sell signals
# up arrow when we buy one share
plt.plot(buys.index, plotDf.loc[buys.index][‘Adj Close’], ‘^’, markersize=10, color=’red’, lw=2., label=’Buy’);
# down arrow when we sell one share
plt.plot(sells.index, plotDf.loc[sells.index][‘Adj Close’], ‘v’, markersize = 10, color=’green’, lw=2., label=’Sell’);
plt.ylabel(‘Price (USD)’); plt.xlabel(‘Date’);
plt.title(‘Buy and Sell signals since 2021′); plt.legend(loc=’best’);
plt.show()

Вот полный блок кода.

def dataExtract(startDate,outputFile):
try:
data = pd.read_pickle(outputFile)
print(‘File data found…reading CrudeOil data’)
except FileNotFoundError:
print(‘File not found…downloading CrudeOil data’)
data = web.DataReader(‘CL=F’, ‘yahoo’, startDate)
data.to_pickle(outputFile)
return data

data = dataExtract(startDate=’2010-01-01′, outputFile=’CrudeOilPrice.pkl’)def MACrossOver(data, shortWindow, longWindow):
signals = pd.DataFrame(index=data.index)
signals[‘tradeSignal’] = 0.0
signals[‘MA20’] = data[‘Close’].rolling(window=shortWindow,
min_periods=1, center=False).mean()
signals[‘MA100’] = data[‘Close’].rolling(window=longWindow,
min_periods=1, center=False).mean()
signals[‘tradeSignal’][shortWindow:] = np.where(signals[‘MA20’][shortWindow:] > signals[‘MA100’][shortWindow:], 1.0, 0.0)
signals[‘finalSignal’] = signals[‘tradeSignal’].diff()
return signalsnewSeries = MACrossOver(data,20,100)newSeries = newSeries[‘2020-01-01’:]
DATA = data[‘2020-01-01’:]
fig = plt.figure(figsize= (15,6))
ax1 = fig.add_subplot(111, ylabel=’CrudeOil price in $’)
DATA[«Adj Close»].plot(ax=ax1, lw=.6)
newSeries[«MA20»].plot(ax=ax1, lw=2.)
newSeries[«MA100»].plot(ax=ax1, lw=2.)
ax1.plot(newSeries.loc[newSeries.finalSignal== 1.0].index, DATA[«Adj Close»][newSeries.finalSignal == 1.0],’^’, markersize=7, color=’green’)
ax1.plot(newSeries.loc[newSeries.finalSignal== -1.0].index, DATA[«Adj Close»][newSeries.finalSignal == -1.0],’v’, markersize=7, color=’red’)
plt.legend([«Adj Close Price»,»Short mavg»,»Long mavg»,»BuySignal»,»SellSignal»])
plt.title(«Simple Moving Average Crossover Strategy»)
plt.show()data[’ema5′] = (data[‘Adj Close’].ewm(span=5,adjust=True,ignore_na=True).mean())
data[’ema20′] = (data[‘Adj Close’].ewm(span=20,adjust=True,ignore_na=True).mean())
data.dropna(inplace=True)def plot():
t = data[‘2020-01-01’:].copy()
fig = go.Figure(data=[go.Candlestick(x=t.index,
open=t[‘Open’],
high=t[‘High’],
low=t[‘Low’],
close=t[‘Adj Close’])])
fig.add_trace(go.Scatter(x = t.index, y = t.ema5, marker = dict(
color = «blue»), name = «EMA5»))
fig.add_trace(go.Scatter(x = t.index, y = t.ema20, marker = dict(
color = «gray»), name = «EMA20»))
fig.update_xaxes(showline=True, linewidth=2, linecolor=’black’, mirror=True)
fig.update_yaxes(showline=True, linewidth=2, linecolor=’black’, mirror=True)
fig.update_layout(autosize = False, width = 1200, height = 600,)
fig.update_layout(title=’Candlestick plot::Crude oil prices’, yaxis_title='(US$)’)
return figfig = plot()
fig.show()def classificationFeatures(data):
data[‘CloseR’] = data[‘Adj Close’].pct_change()
data[‘LowR’] = data.Low.pct_change()
data[‘HighR’] = data.High.pct_change()
data[‘VolumeR’] = data.Volume.pct_change()
data[‘dailyChange’] = (data[‘Adj Close’] — data[‘Open’]) / data[‘Open’]
data[‘oc’] = (data[‘Open’] — data[‘Adj Close’])
data[‘hl’] = (data[‘High’] — data[‘Low’])
data[‘target’] = np.where(data[‘Adj Close’].shift(-1) > data[‘Adj Close’], 1, 0)
data = data.replace([np.inf, -np.inf], np.nan)
data = data.dropna()
return datadata = classificationFeatures(data)
storeDf = data[[‘Open’, ‘High’, ‘Low’, ‘Adj Close’, ‘Volume’]]def features():
X = data[[‘oc’, ‘hl’, ‘LowR’, ‘HighR’, ‘VolumeR’]]
Y = data.target
return X,Y
X,Y = features()gkcv = GapKFold(n_splits=5, gap_before=2, gap_after=1)
for train_index, test_index in gkcv.split(X):
X_train, X_test = X.values[train_index], X.values[test_index]
y_train, y_test = Y[train_index], Y[test_index]RF = RandomForestClassifier(bootstrap=True,
ccp_alpha=0.0,
class_weight=»balanced»,
max_leaf_nodes=None,
min_impurity_decrease=0.0,
min_impurity_split=None,
min_samples_leaf=1,
min_samples_split=2,
min_weight_fraction_leaf=0.0,
n_estimators=1000,
random_state=0,
verbose=0,
warm_start=False).fit(X,Y)
Score = cross_val_score(RF, X, Y, cv=gkcv).mean()storeDf[‘PredictedSignal’] = (RF.predict(X))
storeDf[‘finalSignal’] = storeDf[‘PredictedSignal’].diff()plotDf = storeDf[‘2021-01-01’:]
print(plotDf.finalSignal.value_counts())# Buy/Sell signals plot
buys = plotDf.loc[plotDf[«finalSignal»] == 1];
sells = plotDf.loc[plotDf[«finalSignal»] == -1];
# Plot
fig = plt.figure(figsize=(20, 5));
plt.plot(plotDf.index, plotDf[‘Adj Close’], lw=2., label=’Price’);
# Plot buy and sell signals
# up arrow when we buy one share
plt.plot(buys.index, plotDf.loc[buys.index][‘Adj Close’], ‘^’, markersize=10, color=’red’, lw=2., label=’Buy’);
# down arrow when we sell one share
plt.plot(sells.index, plotDf.loc[sells.index][‘Adj Close’], ‘v’, markersize = 10, color=’green’, lw=2., label=’Sell’);
plt.ylabel(‘Price (USD)’); plt.xlabel(‘Date’);
plt.title(‘Buy and Sell signals since 2021′); plt.legend(loc=’best’);
plt.show()

Заключение:

Мы показали здесь упрощенный вариант генерации сигналов на покупку/продажу на основе простых скользящих средних, экспоненциальных скользящих средних и классификационной модели машинного обучения. Это простой и фундаментальный процесс. Технические индикаторы, такие как MACD, ROC и т.д. могут быть добавлены на основе торговой стратегии. Более того, после того, как стратегия определена и сигналы сгенерированы, следующей важной частью является оценка стратегии. Методы оценки можно найти здесь.

Источник