backtest di una strategia con l'Ichimoku in Python

Backtest di una strategia con l’Ichimoku in Python – Parte 2

Sommario

Questo articolo è la seconda parte della mini-serie che descrive come implementare il backtest di una strategia con l’Ichimoku in Python. Nella prima parte abbiamo descritto come calcolare e visualizzare il grafico degli elementi Ichimoku. Vediamo ora come implementare la logica della strategia di trading sistematico e del successivo backtest in python.

La strategia

L’approccio Ichimoku si basa su due elementi principali:

  1. i segnali e le indicazioni prodotte dalla “nuvola”, che è creata dall’interazione tra Senkou Span A e Senkou Span B (e talvolta la sua relazione con il prezzo
  2. l’interazione tra il prezzo, il Tenkan-sen e il Kijun-sen.

Esaminiamo ora il primo elemento, cioè i segnali prodotti dalla nuvola. Esistono due modi per identificare la tendenza generale tramite la nuvola. In primo luogo, il trend è al rialzo quando i prezzi sono sopra la nuvola, al ribasso quando i prezzi sono al di sotto della nuvola e piatto quando i prezzi sono nella nuvola. In secondo luogo, il trend rialzista si rafforza quando la Senkou Span A (la linea verde della nuvola) è in aumento ed è sopra la Senkou Span B (la linea  rossa della nuvola). Questo scenario produce una nuvola verde. Al contrario, un trend al ribasso si rafforza quando la Leading Span A (la linea verde della nuvola) scende e si trova al di sotto della Leading Span B (la linea rossa della nuvola). Questo scenario produce una nuvola rossa perchè la nuvola è proiettata in avanti di 26 giorni.

Passiamo ora al secondo elemento, l’interazione tra il prezzo, il Tenkan-sen e il Kijun-sen. Il prezzo, il Tenkan-sen e il Kijun-sen sono usati per identificare i segnali più veloci e frequenti. È importante ricordare che i segnali rialzisti sono più forti quando i prezzi sono al di sopra della nuvola e la nuvola è verde. I segnali ribassisti sono più forti quando i prezzi sono al di sotto della nuvola e la nuvola è rossa. In altre parole, i segnali rialzisti sono preferiti quando il trend principale è al rialzo (prezzi sopra la nuvola verde), mentre i segnali ribassisti sono preferiti quando il trend principale è al ribasso (i prezzi sono sotto la nuvola rossa). Questa è la regola fondamentale del trading nella direzione del trend principale.

I segnali contrari al trend in essere sono ritenuti più deboli, come segnali rialzisti a breve termine all’interno di un trend al ribasso di lungo termine o segnali ribassisti a breve termine all’interno di un trend al rialzo di lungo termine. Quando il Tenkan-sen attraversa il Kijun-sen è considerato un segnale rialzista e viceversa quando il Tenkan-sen attraversa il Kijun-sen è considerato un segnale ribassista.

Per quanto riguarda il prezzo e il Tenkan-sen, quando i prezzi attraversano il Tenkan-sen è considerato un segnale rialzista, e viceversa quando i prezzi attraversano il Tenkan-sen  considerato un segnale ribassista.

In sintesi, la strategia con l’ichimoku in python prevede le seguenti regole.

Si apre una posizione Long quando le seguenti condizioni sono vere:

  1. I prezzi sono sopra la nuvola.
  2. Senkou Span A è sopra Senkou Span B
  3. il Tenkan-sen attraversa il Kijun-sen OPPURE i prezzi attraversano il Tenkan-sen.

Quando questi criteri sono soddisfatti si invia un ordine di acquisto all’apertura della candela successiva. Si chiude una posizione Long quando il Tenkan-sen attraversa il Kijun-sen.

Si apre una posizione Short quando le seguenti condizioni sono vere:

  1. I prezzi sono al di sotto della nuvola
  2. Il Senkou Span A è al di sotto del Senkou Span B
  3. il Tenkan-sen attraversa il Kijun-sen OPPURE i prezzi attraversano il Tenkan-sen

Quando questi criteri sono soddisfatti si invia un ordine di vendita all’apertura della candela  successiva.  Si chiede una posizione Short quanto il Tenkan-sen attraversa il Kijun-sen.

L’implementazione

Vediamo come implementare il backtest della strategia con l’ichimoku in python. Iniziamo importando le librerie necessarie e recuperiamo i dati storici dei prezzi delle azioni su cui vogliamo testare la strategia. Per completezza riportiamo le parti di codice descritte nell’articolo precedente utili per creare gli elementi dell’Ichimoku e un grafico Plotly della serie risultante.

				
					import pandas as pd
import numpy as np
import yfinance as yf
import matplotlib.dates as dates
import plotly.graph_objs as go

start = '2010-01-01'
end = '2020-01-01'
d = yf.download("AAPL", start, end)
# converte le date in valori interi da usare per le
# funzioni dei grafici a candele di matplotlib
d['Dates'] = dates.date2num(d.index)

# Tenkan-sen (Conversion Line): (massimo di 9 periodi + minimo di 9 periodi) / 2
nine_period_high = d['High'].rolling(window= 9).max()
nine_period_low = d['Low'].rolling(window= 9).min()
d['tenkan_sen'] = (nine_period_high + nine_period_low) /2

# Kijun-sen (Base Line): (massimo di 26 periodi + minimo di 26 periodi) / 2
period26_high = d['High'].rolling(window=26).max()
period26_low = d['Low'].rolling(window=26).min()
d['kijun_sen'] = (period26_high + period26_low) / 2

# Senkou Span A (Leading Span A): (Conversion Line + Base Line) / 2
d['senkou_span_a'] = ((d['tenkan_sen'] + d['kijun_sen']) / 2).shift(26)

# Senkou Span B (Leading Span B): (massimo di 52 periodi + minimo di 52 periodi low) / 2
period52_high = d['High'].rolling(window=52).max()
period52_low = d['Low'].rolling(window=52).min()
d['senkou_span_b'] = ((period52_high + period52_low) / 2).shift(26)

# Proiezione dei prezzi di chiusura indietro di 26 periodo
d['chikou_span'] = d['Close'].shift(-26)


# Imposta i colori per le candere up e down
INCREASING_COLOR = '#00ff00'
DECREASING_COLOR = '#ff0000'
# Crea la lista che contiene i dizionari con i dati
# della prima serie di dati da visualizzare
data1 = [dict(type='candlestick',
    open=d.Open,
    high=d.High,
    low=d.Low,
    close=d.Close,
    x=d.index,
    yaxis='y2',
    name='AAPL',
    increasing=dict(line=dict(color=INCREASING_COLOR)),
    decreasing=dict(line=dict(color=DECREASING_COLOR)),
)]
# Crea un dizionario vuoto per contentere le impostazioni e il layout
layout = dict()
# Crea l'oggetto principale "Figure" che contiene i dati da visualizzare e le impostazioni
fig = dict(data=data1, layout=layout)
# Assegna vari valori di impoestazioni - colore di sfondo, range di selezione, ecc
fig['layout']['plot_bgcolor'] = 'rgb(250, 250, 250)'
fig['layout']['xaxis'] = dict(rangeselector=dict(visible=True))
fig['layout']['yaxis'] = dict(domain=[0, 0.2], showticklabels=False)
fig['layout']['yaxis2'] = dict(domain=[0.2, 0.8])
fig['layout']['legend'] = dict(orientation='h', y=0.9, x=0.3, yanchor='bottom')
fig['layout']['margin'] = dict(t=40, b=40, r=40, l=40)

# Popola l'oggetto "rangeselector" con le impostazioni necessarie
rangeselector = dict(
    visible=True,
    x=0, y=0.9,
    bgcolor='rgba(150, 200, 250, 0.4)',
    font=dict(size=13),
    buttons=list([
        dict(count=1,
             label='reset',
             step='all'),
        dict(count=1,
             label='1yr',
             step='year',
             stepmode='backward'),
        dict(count=3,
             label='3 mo',
             step='month',
             stepmode='backward'),
        dict(count=1,
             label='1 mo',
             step='month',
             stepmode='backward'),
        dict(step='all')
    ]))

fig['layout']['xaxis']['rangeselector'] = rangeselector
# Aggiunge gli elementi Ichimoku nel grafico
fig['data'].append(dict(x=d['tenkan_sen'].index, y=d['tenkan_sen'], type='scatter', mode='lines',
                        line=dict(width=1),
                        marker=dict(color='#33BDFF'),
                        yaxis='y2', name='tenkan_sen'))
fig['data'].append(dict(x=d['kijun_sen'].index, y=d['kijun_sen'], type='scatter', mode='lines',
                        line=dict(width=1),
                        marker=dict(color='#F1F316'),
                        yaxis='y2', name='kijun_sen'))
fig['data'].append(dict(x=d['senkou_span_a'].index, y=d['senkou_span_a'], type='scatter', mode='lines',
                        line=dict(width=1),
                        marker=dict(color='#228B22'),
                        yaxis='y2', name='senkou_span_a'))
fig['data'].append(dict(x=d['senkou_span_b'].index, y=d['senkou_span_b'], type='scatter', mode='lines',
                        line=dict(width=1), fill='tonexty',
                        marker=dict(color='#e99653'),
                        yaxis='y2', name='senkou_span_b'))
fig['data'].append(dict(x=d['chikou_span'].index, y=d['chikou_span'], type='scatter', mode='lines',
                        line=dict(width=1),
                        marker=dict(color='#D105F5'),
                        yaxis='y2', name='chikou_span'))

# Imposta la lista dei colori per le candele
colors = []
for i in range(len(d.Close)):
    if i != 0:
        if d.Close[i] > d.Close[i - 1]:
            colors.append(INCREASING_COLOR)
        else:
            colors.append(DECREASING_COLOR)
    else:
        colors.append(DECREASING_COLOR)

grafico = go.Figure(fig)
grafico.show()

				
			
backtest di una strategia con l'Ichimoku in Python

Abbiamo generato correttamente il dataframe dei prezzi degli elementi dell’indicatore Ichimoku.  Vediamo ora come implementare la logica della strategia. Per prima cosa eliminiamo tutti i valori “Na” nel dataframe e quindi impostiamo il “regimare marker” per indicare quando i prezzi sono al di sopra, al di sotto o all’interno della nuvola.  Usiamo lo stesso approccio per indicare se il Senkou Span A è sopra o sotto il Senkou Span B.

				
					d.dropna(inplace=True)
d['above_cloud'] = 0
d['above_cloud'] = np.where((d['Low'] > d['senkou_span_a'])  & (d['Low'] > d['senkou_span_b'] ), 1, d['above_cloud'])
d['above_cloud'] = np.where((d['High'] < d['senkou_span_a']) & (d['High'] < d['senkou_span_b']), -1, d['above_cloud'])
d['A_above_B'] = np.where((d['senkou_span_a'] > d['senkou_span_b']), 1, -1)
				
			

Dobbiamo quindi creare due nuove colonne nel dataframe. La prima colonna identifica quando si ha un incrocio tra il Tenkan-sen e il Kiju-sen. Inseriamo 1 se il Tenkan-sen incrocia il Kijun-sen verso l’alto, mentre inseriamo -1 se abbiamo un incrocio verso il basso. La  seconda colonna identifica  quando il prezzo “Open”  incrocia la linea Tenkan-sn. Anche in questo caso inseriamo 1 quando abbiamo un incrocio verso l’alto e -1 quando abbiamo un incrocio verso il basso.

				
					d['tenkan_kiju_cross'] = np.NaN
d['tenkan_kiju_cross'] = np.where((d['tenkan_sen'].shift(1) <= d['kijun_sen'].shift(1)) & (d['tenkan_sen'] > d['kijun_sen']), 1, d['tenkan_kiju_cross'])
d['tenkan_kiju_cross'] = np.where((d['tenkan_sen'].shift(1) >= d['kijun_sen'].shift(1)) & (d['tenkan_sen'] < d['kijun_sen']), -1, d['tenkan_kiju_cross'])

d['price_tenkan_cross'] = np.NaN
d['price_tenkan_cross'] = np.where((d['Open'].shift(1) <= d['tenkan_sen'].shift(1)) & (d['Open'] > d['tenkan_sen']), 1, d['price_tenkan_cross'])
d['price_tenkan_cross'] = np.where((d['Open'].shift(1) >= d['tenkan_sen'].shift(1)) & (d['Open'] < d['tenkan_sen']), -1, d['price_tenkan_cross'])
				
			

Il seguente codice implementa il calcolo e la creazione dei segnali ingresso per l’acquisto e la vendita.

Il Backtest

Per prima cosa creiamo una colonna buy e la inizialiamo con valori np.NaN. Per i segnali long impostiamo a 1 il valore di una riga di questa colonna se i primi due criteri sono soddisfatti (i prezzi sono al di sopra della nuvola e il Senkou Span A è sopra il Senkou Span B) e almeno uno degli ultimi 2 criteri è soddisfatto (il Tenkan-sen attraversa il Kijun-sen o prezzi attraversano il Tenkan-sen). Quando questi criteri non sono soddisfatti il valore nella colonna buy rimane np.NaN.

Identifichiamo le righe della colonna buy che soddisfano i criteri di chiusura, ovvero un incrocio del Tenkan-sen con il Kijun-sen. Valorizziamo con 0 queste righe della colonna buy. Usiamo quindi il metodo ffill per “riempire in avanti” i valori di colonna buy e quindi sostituire le celle che contengono valori np.NaN. In questo modo la colonna prevede un valore di 1 quando abbiamo una posizione long aperta, mentre prevede uno 0 quando non abbiamo posizioni aperte.

La stessa logica viene quindi ripetuta per valorizzare la colonna sell con i segnali, ovviamente invertiti, per implementare correttamente la logica di strategia sul lato “short”.

Infine sommiamo le colonne buy e sell per creare una nuova colonna che rappresenta la posizione della strategia.

				
					d['buy'] = np.NaN
d['buy'] = np.where((d['above_cloud'].shift(1) == 1) & (d['A_above_B'].shift(1) == 1) & ((d['tenkan_kiju_cross'].shift(1) == 1) | (d['price_tenkan_cross'].shift(1) == 1)), 1, d['buy'])
d['buy'] = np.where(d['tenkan_kiju_cross'].shift(1) == -1, 0, d['buy'])
d['buy'].ffill(inplace=True)

d['sell'] = np.NaN
d['sell'] = np.where((d['above_cloud'].shift(1) == -1) & (d['A_above_B'].shift(1) == -1) & ((d['tenkan_kiju_cross'].shift(1) == -1) | (d['price_tenkan_cross'].shift(1) == -1)), -1, d['sell'])
d['sell'] = np.where(d['tenkan_kiju_cross'].shift(1) == 1, 0, d['sell'])
d['sell'].ffill(inplace=True)

d['position'] = d['buy'] + d['sell']
				
			
Successivamente dobbiamo  creare una colonna stock_returns che contiene i rendimenti logaritmici del titolo azionario su cui stiamo eseguendo il backtest. Questi rendimenti sono moltiplicati per la position della strategia per calcolare i rendimenti della strategia a seconda che siamo long o short. Quindi effettuiamo la somma cumulativa delle due serie di rendimenti e le tracciamo su un grafico per un confronto.
				
					d['stock_returns'] = np.log(d['Open']) - np.log(d['Open'].shift(1))
d['strategy_returns'] = d['stock_returns'] * d['position']
d[['stock_returns','strategy_returns']].cumsum().plot(figsize=(15,8))
plt.show()
				
			
Backtest-Strategia-Ichimoku-Strategy

Per consentirci di testare la strategia su diversi titoli/asset, spostiamo il codice all’intero di una funzione che può essere richiamata con diversi simboli ticker e intervalli di date passati come parametri di ingresso.

				
					
def ichimoku(ticker, start, end):
    d=yf.download(ticker, start, end)[['Open','High','Low','Close']]
    # Tenkan-sen (Conversion Line)
    nine_period_high = d['High'].rolling(window= 9).max()
    nine_period_low = d['Low'].rolling(window= 9).min()
    d['tenkan_sen'] = (nine_period_high + nine_period_low) /2
    # Kijun-sen (Base Line)
    period26_high = d['High'].rolling(window=26).max()
    period26_low = d['Low'].rolling(window=26).min()
    d['kijun_sen'] = (period26_high + period26_low) / 2
    # Senkou Span A (Leading Span A)
    d['senkou_span_a'] = ((d['tenkan_sen'] + d['kijun_sen']) / 2).shift(26)
    # Senkou Span B (Leading Span B)
    period52_high = d['High'].rolling(window=52).max()
    period52_low = d['Low'].rolling(window=52).min()
    d['senkou_span_b'] = ((period52_high + period52_low) / 2).shift(52)
    # Chikou Span
    d['chikou_span'] = d['Close'].shift(-26)
    d.dropna(inplace=True)
    d['above_cloud'] = 0
    d['above_cloud'] = np.where((d['Low'] > d['senkou_span_a'])  & (d['Low'] > d['senkou_span_b'] ), 1, d['above_cloud'])
    d['above_cloud'] = np.where((d['High'] < d['senkou_span_a']) & (d['High'] < d['senkou_span_b']), -1, d['above_cloud'])
    d['A_above_B'] = np.where((d['senkou_span_a'] > d['senkou_span_b']), 1, -1)
    d['tenkan_kiju_cross'] = np.NaN
    d['tenkan_kiju_cross'] = np.where((d['tenkan_sen'].shift(1) <= d['kijun_sen'].shift(1)) & (d['tenkan_sen'] > d['kijun_sen']), 1, d['tenkan_kiju_cross'])
    d['tenkan_kiju_cross'] = np.where((d['tenkan_sen'].shift(1) >= d['kijun_sen'].shift(1)) & (d['tenkan_sen'] < d['kijun_sen']), -1, d['tenkan_kiju_cross'])
    d['price_tenkan_cross'] = np.NaN
    d['price_tenkan_cross'] = np.where((d['Open'].shift(1) <= d['tenkan_sen'].shift(1)) & (d['Open'] > d['tenkan_sen']), 1, d['price_tenkan_cross'])
    d['price_tenkan_cross'] = np.where((d['Open'].shift(1) >= d['tenkan_sen'].shift(1)) & (d['Open'] < d['tenkan_sen']), -1, d['price_tenkan_cross'])
    d['buy'] = np.NaN
    d['buy'] = np.where((d['above_cloud'].shift(1) == 1) & (d['A_above_B'].shift(1) == 1) & ((d['tenkan_kiju_cross'].shift(1) == 1) | (d['price_tenkan_cross'].shift(1) == 1)), 1, d['buy'])
    d['buy'] = np.where(d['tenkan_kiju_cross'].shift(1) == -1, 0, d['buy'])
    d['buy'].ffill(inplace=True)

    d['sell'] = np.NaN
    d['sell'] = np.where((d['above_cloud'].shift(1) == -1) & (d['A_above_B'].shift(1) == -1) & ((d['tenkan_kiju_cross'].shift(1) == -1) | (d['price_tenkan_cross'].shift(1) == -1)), -1, d['sell'])
    d['sell'] = np.where(d['tenkan_kiju_cross'].shift(1) == 1, 0, d['sell'])
    d['sell'].ffill(inplace=True)
    d['position'] = d['buy'] + d['sell']
    d['stock_returns'] = np.log(d['Open']) - np.log(d['Open'].shift(1))
    d['strategy_returns'] = d['stock_returns'] * d['position']
    d[['stock_returns','strategy_returns']].cumsum().plot(figsize=(15,8))
				
			

Ad esempio possiamo fare un backtest sul titolo Netflix nel periodo dal 2000 al 2020.

				
					ticker = 'NFLX'
start = '2000-01-01'
end = '2020-01-01'
ichimoku(ticker, start, end)
				
			
Backtest-Strategia-Ichimoku-Netflix

Abbiamo quindi implementato le funzionalità necessarie per ottenere il risultato desiderato. Ovviamente la strategia può essere adattata e migliorata modificando le regole e la logica, magari impostando un elemento di stop loss o modificando i criteri di entrata/uscita. Potete usare questo codice come base per i vostri esperimenti.

Codice completo

In questo articolo abbiamo descritto come implementare il backtest di una strategia con l’Ichimoku in Python. Per il codice completo riportato in questo articolo, si può consultare il seguente repository di github:
https://github.com/datatrading-info/Backtest_Strategie

Benvenuto su DataTrading!

Sono Gianluca, ingegnere software e data scientist. Sono appassionato di coding, finanza e trading. Leggi la mia storia.

Ho creato DataTrading per aiutare le altre persone ad utilizzare nuovi approcci e nuovi strumenti, ed applicarli correttamente al mondo del trading.

DataTrading vuole essere un punto di ritrovo per scambiare esperienze, opinioni ed idee.

SCRIVIMI SU TELEGRAM

Per informazioni, suggerimenti, collaborazioni...

Torna in alto
Scroll to Top