Créer des stratégies avec des données réelles
Avant-propos
Description
/trading/historiques/ctrl_histos.py
:
# Imports externes
import os # http://www.python-simple.com/python-modules-fichiers/os-path.php
import datetime
# Imports internes
from functions.utils import DateTime
class CtrlHistos:
def __init__(self, instrument):
""" https://github.com/fxcm/MarketData
Tous les symboles :
AUDCAD, AUDCHF, AUDJPY, AUDNZD, AUDUSD, CADCHF, CADJPY, EURAUD, EURCHF,
EURGBP, EURJPY, EURNZD, EURUSD, GBPCAD, GBPCHF, GBPJPY, GBPNZD, GBPUSD,
NZDCAD, NZDCHF, NZDJPY, NZDUSD, USDCAD, USDCHF, USDJPY, USDTRY
Les plus utilisés (Dans l'ordre d'importance) :
EURUSD, USDJPY, EURCHF, USDCAD, NZDUSD, EURGBP, EURJPY, GBPJPY, GBPCHF, GBPUSD """
self.instrument = instrument # EUR/USD
self.symbol = instrument.replace('/', '') # EURUSD - Suppression de '/'
self.dt = DateTime()
self.week_delta = datetime.timedelta(weeks=1) # Durée : <class 'datetime.timedelta'>
""" Les données peuvent se trouver sur un disque dur SSD autre que celui en cours.
Le cas échéant, db_path doit être modifié (chemin absolu).
Il est également possible, pour les besoins des tests, d'avoir plusieurs bases de données.
|_ Il faudra alors modifier db_path chaque fois que l'on désire switcher. """
db_path = os.path.dirname(__file__) # <--- Chemin par défaut, à modifier si autre disque dur.
self.symbol_dir = os.path.abspath(f'{db_path}/db/{self.symbol}') # Dossier des fichiers d'historiques.
def main():
h = CtrlHistos('EUR/USD')
# ... Placer ici du code de test et de MAP.
print('... Ici, code de test ...')
# h.download_histos(nb_weeks=1, b_ticks=True)
# h.verify_weeks()
if __name__ == '__main__':
main()
db_path
dans l'ini, si vous préférez utiliser un disque SSD
réservé à cela. 2 raisons :
SSD
(interne de préférence) est plus rapide./trading/historiques/db
.
clic droit > Run ctrl_histos
.
str
. Exemples pour dtstr : "2016-08-22" ou '20/11/2014 8:20', etc. ← type str.stamp
. Exemple : dtstamp = 1471816800.0 ← type float.objet
: o_d = objet <class 'datetime.date'> ← objet date.objet
: o_dt = objet <class 'datetime.datetime'> ← objet datetime.get_dtstamp_from_dtstr()
.get_dtstr_from_dtstamp()
.get_date_from_dtstamp()
.get_dtstamp_from_date()
.get_datetime_from_dtstamp()
.get_dtstamp_from_datetime()
.get_datetime_from_date()
.get_date_from_datetime()
.Les objets o_d
et o_dt
bénéficient de nombreuses méthodes. Voir documentation Python.
utils.py
contient déjà la classe DateTime
. Nous allons la compléter en ajoutant ces méthodes./functions/utils.py > DateTime
:
@staticmethod
def get_dtstamp_from_dtstr(dt_str, dt_format="%m/%d/%Y %H:%M:%S.%f"):
""" Fonction inverse de get_dtstr_from_dtstamp(). Précision à la microseconde.
@param dt_str: Chaîne de caractères représentant une date-heure au format 'humain'.
@param dt_format: Format par défaut, dicté par celui utilisé dans les fichiers d'historiques téléchargés.
@return: Nombre réel au format stamp unix → https://www.unixtimestamp.com/
"""
dt_stamp = datetime.datetime.strptime(dt_str, dt_format)
time_stamp = time.mktime(dt_stamp.timetuple())
micro_s = dt_stamp.microsecond / 1000_000
return time_stamp + micro_s
@staticmethod
def get_dtstr_from_dtstamp(dt_stamp, dt_format="%m/%d/%Y %H:%M:%S"):
""" Fonction inverse de get_dtstamp_from_dtstr() """
micro_s = int((dt_stamp % 1) * 1_000_000) # Partie décimale * 1 000 000.
dt_stamp = int(dt_stamp)
structured_time = time.localtime(dt_stamp)
date_str = time.strftime(dt_format, structured_time)
return f'{date_str}.{micro_s}' if '%S' in dt_format else date_str
@staticmethod
def get_date_from_dtstamp(dt_stamp):
""" dtstamp --> o_date """
return datetime.date.fromtimestamp(dt_stamp) # Conversion timestamp en objet <class 'datetime.date'>
def get_dtstamp_from_date(self, o_date):
""" L'objet o_date ne possède pas d'attribut 'timestamp' => Conversion préalable en o_datetime """
o_datetime = self.get_datetime_from_date(o_date)
return o_datetime.timestamp()
@staticmethod
def get_datetime_from_dtstamp(dt_stamp):
return datetime.datetime.fromtimestamp(dt_stamp) # Conversion timestamp en objet <class 'datetime.datetime'>
@staticmethod
def get_dtstamp_from_datetime(o_datetime):
return o_datetime.timestamp()
@staticmethod
def get_datetime_from_date(o_date):
return datetime.datetime.combine(o_date, datetime.datetime.min.time())
@staticmethod
def get_date_from_datetime(o_datetime):
return o_datetime.date()
utils.py
:
if __name__ == '__main__':
dt = DateTime()
dtstamp1 = dt.get_dtstamp_from_dtstr('20-11-2014 8:20:15.64', dt_format="%d-%m-%Y %H:%M:%S.%f")
print(1, 'dtstamp1 :', dtstamp1)
dtstr = dt.get_dtstr_from_dtstamp(dtstamp1, dt_format="%d/%m/%Y")
print(2, 'dtstr :', dtstr)
o_d = dt.get_date_from_dtstamp(dtstamp1)
print(3, 'o_d :', o_d, type(o_d))
dtstamp2 = dt.get_dtstamp_from_date(o_d) # get_datetime_from_date() est utilisée, donc également testée.
print(4, 'dtstamp2 :', dtstamp2)
o_dt = dt.get_datetime_from_dtstamp(dtstamp1)
print(5, 'o_dt :', o_dt, type(o_dt))
dtstamp3 = dt.get_dtstamp_from_datetime(o_dt)
print(6, 'dtstamp3 :', dtstamp3)
o_d = dt.get_date_from_datetime(o_dt)
print(7, 'o_d :', o_d, type(o_d))
Ce code peut être supprimé à la fin des tests.
Run 'utils'
._get_weeks_list()
. Algorithme :
download_histos(nb_weeks, b_ticks)
. Algorithme :
nb_weeks
). A chaque tour :
_next_week_required()
la semaine à télécharger, sous forme de tuple (année, num semaine).True
, cela signifie que la base est déjà à jour ⇒ Message et return
.False
, le fichier demandé n'existe pas sur internet ⇒ Message et return
._download_week()
, qui procède au téléchargement d'un seul fichier._next_week_required()
:
.csv
dans le disque dur.download_histos()
._next_week_required()
._download_week()
._get_weeks_list()
._get_csv_path()
._get_url()
.gzip
requests
← à installer.io
pandas
← à installer./trading/historiques/ctrl_histos.py
:
# Imports externes
import datetime
import os # http://www.python-simple.com/python-modules-fichiers/os-path.php
import gzip
import requests
from io import BytesIO, StringIO
import pandas as pd
# Imports internes
from functions.utils import DateTime
class CtrlHistos:
def __init__(self, instrument):
""" https://github.com/fxcm/MarketData
Tous les symboles :
AUDCAD, AUDCHF, AUDJPY, AUDNZD, AUDUSD, CADCHF, CADJPY, EURAUD, EURCHF,
EURGBP, EURJPY, EURNZD, EURUSD, GBPCAD, GBPCHF, GBPJPY, GBPNZD, GBPUSD,
NZDCAD, NZDCHF, NZDJPY, NZDUSD, USDCAD, USDCHF, USDJPY, USDTRY
Les plus utilisés (Dans l'ordre d'importance) :
EURUSD, USDJPY, USDCHF, EURCHF, USDCAD, NZDUSD, EURGBP, EURJPY, GBPJPY, GBPCHF """
self.instrument = instrument # EUR/USD
self.symbol = instrument.replace('/', '') # EURUSD - Suppression de '/'
self.dt = DateTime()
self.week_delta = datetime.timedelta(weeks=1) # Durée : <class 'datetime.timedelta'>
""" Les données peuvent se trouver sur un disque dur SSD autre que celui en cours.
Le cas échéant, db_path doit être modifié (chemin absolu).
Il est également possible, pour les besoins des tests, d'avoir plusieurs bases de données.
|_ Il faudra alors modifier db_path chaque fois que l'on désire switcher. """
db_path = os.path.dirname(__file__) # <--- Chemin par défaut, à modifier si autre disque dur.
self.symbol_dir = os.path.abspath(f'{db_path}/db/{self.symbol}') # Dossier des fichiers d'historiques.
def _get_url(self, year, week, b_ticks):
url_candle = 'https://candledata.fxcorporate.com/m1'
url_tick = 'https://tickdata.fxcorporate.com'
url_data = f'{url_tick if b_ticks else url_candle}/{self.symbol}/{year}/{week}.csv.gz'
req = requests.head(url_data)
""" Correction dûe au non-respect du format ISO par fxcorporate. """
if week == 1 and req.status_code == 404:
""" fxcorporate ne respecte pas les numéros de semaines ISO. Exemple :
- le fichier '.../2019/53.csv.gz' existe en téléchargement ...
|_ ... alors que la semaine 53 n'existe pas en 2019.
- le fichier '.../2020/1.csv.gz' n'existe pas en téléchargement. """
year, week = year-1, 53
url_data = f'{url_tick if b_ticks else url_candle}/{self.symbol}/{year}/{week}.csv.gz'
""" Test de la présence du fichier sur internet. Seule l'entête (requests.head) est demandée. """
req = requests.head(url_data)
return url_data, req.status_code # req.status_code == 200 ou 404.
def _get_csv_path(self, year, week, b_ticks):
str_week = f'0{week}'[-2:]
file_name = f'tick_{str_week}.csv' if b_ticks else f'candle_{str_week}.csv'
return os.path.join(self.symbol_dir, str(year), file_name)
def _get_weeks_list(self, b_ticks):
""" Renvoie une liste de tuples (année, num_semaine). Certaines années ont 53 semaines. """
""" Premier mercredi (~ milieu de la semaine). Les ticks ont des dates de début différentes, d'où le dict. """
d_min = {'EURUSD': '6/1/2016', 'USDJPY': '4/1/2017', 'USDCHF': '4/1/2017', # Devises principales.
'EURCHF': '6/1/2016', 'USDCAD': '4/1/2017', 'NZDUSD': '4/1/2017',
'EURGBP': '4/1/2017', 'EURJPY': '6/1/2016', 'GBPJPY': '4/1/2017', 'GBPCHF': '4/1/2017'}
wednesday = d_min.get(self.symbol, '7/1/2016') if b_ticks else '4/1/2012'
dtstamp = self.dt.get_dtstamp_from_dtstr(wednesday, dt_format="%d/%m/%Y")
o_d = self.dt.get_date_from_dtstamp(dtstamp)
l_weeks = list()
o_today = datetime.date.today()
while o_d < o_today:
d_iso = o_d.isocalendar()
l_weeks.append((d_iso[0], d_iso[1])) # (année, semaine)
o_d += self.week_delta # Mercredi suivant.
return l_weeks
def _download_week(self, year, week, b_ticks):
""" A partir de l'url on effectue un traitement qui produit un fichier numpy conforme. """
""" Téléchargement du fichier csv.gz depuis les serveurs du brocker FXCM. """
def mess(msg):
print(msg + ' → ', end='')
url_data, status_code = self._get_url(year, week, b_ticks)
if status_code != 200:
return False
mess(f"Téléchargement ({'Ticks' if b_ticks else 'Candles'}, {self.instrument}, {year}, {week})")
req = requests.get(url_data) # Chronophage.
buf = BytesIO(req.content)
mess('Décompactage')
f = gzip.GzipFile(fileobj=buf)
data_csv = f.read()
mess('Décodage')
codec, type_data = ('utf-16', 'tick') if b_ticks else ('utf-8', 'candle')
data_str = data_csv.decode(codec)
data_pd = pd.read_csv(StringIO(data_str)) # Pas de colonne-index (index_col=0 retiré)
mess('Écriture sur disque dur')
file_name = ('tick_' if b_ticks else 'candle_') + f'0{week}'[-2:] + '.csv' # tick_02.csv
""" Création du dossier s'il n'existe pas. """
year_path = os.path.join(self.symbol_dir, str(year))
os.makedirs(year_path, exist_ok=True)
csv_path = os.path.join(year_path, file_name)
data_pd.to_csv(csv_path, index=False)
""" Fin de phrase. """
b_ok = os.path.isfile(csv_path)
print('réussie.\n' if b_ok else 'échouée.\n')
return b_ok
def _next_week_required(self, b_ticks):
""" On cherche sur disque dur, pour le symbole, les dossiers d'historiques (un dossier par année). """
l_dir = next(os.walk(self.symbol_dir))[1] if os.path.isdir(self.symbol_dir) else []
l_dir.sort() # Tri inutile, mais fait par sécurité.
l_weeks = self._get_weeks_list(b_ticks) # Liste des semaines depuis le début.
""" - On sort des 2 boucles au premier fichier trouvé : break et break.
- Parcours des dossiers d'années. Chacun contient les fichiers candle_??.csv et tick_??.csv """
print('Recherche sur internet ...')
b_file_exist = False
for i, (year, week) in enumerate(l_weeks):
file = self._get_csv_path(year, week, b_ticks)
if os.path.isfile(file):
b_file_exist = True
continue
""" Si la base de données est à jour, on ne passe pas par ici, la valeur de retour est True. """
""" Affichage. """
str_week = f'0{week}'[-2:]
print(f'({year}, {str_week}) ', end='\n' if i % 7 == 6 else '')
""" Existence du fichier sur internet. """
url_data, status_code = self._get_url(year, week, b_ticks)
if status_code != 200:
continue
if i % 7 < 6:
print()
return year, week
return b_file_exist
def download_histos(self, nb_weeks, b_ticks):
for _ in range(nb_weeks):
t_week = self._next_week_required(b_ticks=b_ticks)
if isinstance(t_week, tuple):
self._download_week(*t_week, b_ticks=b_ticks)
else:
if t_week: # Booléen == True
print("La base de données est à jour.")
else: # Booléen == False
print(f"\nL'instrument {self.instrument} n'existe pas sur internet.")
return
def main():
h = CtrlHistos('EUR/USD')
h.download_histos(nb_weeks=5, b_ticks=False)
if __name__ == '__main__':
main()
Ne pas oublier d'installer les packages nécessaires.
def main():
h = CtrlHistos('EUR/USD')
h.download_histos(nb_weeks=5, b_ticks=False )
if __name__ == '__main__':
main()
Run 'ctrl_histos'
./trading/historiques/db/
par défaut).
Les fichiers téléchargés sont indiqués par un X. On voit qu'il reste beaucoup de ticks à télécharger.
verify_weeks()
.CtrlHistos.verify_weeks()
:
def verify_weeks(self):
""" Affichage d'un tableau dans la console (Lignes=années, Colonnes=semaines).
'.' = semaine absente, 'X' = semaine présente. """
""" Affichage de la ligne des titres. """
if not os.path.isdir(self.symbol_dir):
print('La base de données est vide.')
return
l_years = next(os.walk(self.symbol_dir))[1]
for typ in ['Ticks', 'Candles']:
""" Affichage du titre et entêtes de colonnes. """
print(f"{self.instrument + '-' + typ :<15} ", end='') # Min 8 caractères, alignés à gauche.
for week in range(1, 54):
num_week = f"{str(week) :<2}" # Min 2 caractères, alignés à gauche.
print(num_week, end=' ')
""" Affichage du contenu - Ordonnées = années, abscisse = semaines. """
b_first = False
for year in l_years:
""" Boucle sur les années présentes dans le disque. """
l_weeks = next(os.walk(os.path.join(self.symbol_dir, year)))[2]
if not b_first and typ[:-1].lower() not in ''.join(l_weeks):
continue
b_first = True
""" Passe à la ligne à la première semaine de l'année {year}. """
print(f"\n{str(year) :<17}", end=' ')
""" Calcul : nb_weeks = nombre de semaines dans l'année {year}. """
o_dat = self.dt.get_date_from_dtstamp(self.dt.get_dtstamp_from_dtstr(f'{year}-12-28', '%Y-%m-%d'))
nb_weeks = o_dat.isocalendar()[1] + 1
for num_week in range(1, nb_weeks):
""" Boucle sur les semaines de l'année {year}. """
file_path = os.path.join(self.symbol_dir, year, f'{typ[:-1].lower()}_'+f'0{num_week}'[-2:]+'.csv')
check = 'X ' if os.path.isfile(file_path) else '. '
print(check, end=' ')
print('\n')
refresh()
, exécutée lorsqu'un paramètre du dockable est modifié.
self.print_weeks()
.self.download_weeks()
.
print_weeks()
pour voir le nouvel état des téléchargements.CtrlHistos
.histos.py
./nodes/generateurs/histos/histos.py
:
# Imports internes
from pc.ctrl_node import CtrlNode
from trading.historiques.ctrl_histos import CtrlHistos
d_datas = {
'name': 'Histos', # Label affiché sous l'icone.
'icon': 'histos.png', # Icone affichée.
}
class Node(CtrlNode):
def __init__(self, o_scene, s_id, pos):
super().__init__(o_scene, s_id, pos)
self.type = 'Histos'
self.setup()
def setup(self, child_file=__file__):
super().setup(child_file)
@property
def ld_outputs(self):
return [{
'label': 'Sortie',
'label_pos': (-38, -10)
}]
def fixed_params(self):
return {
'Instrument': ['EUR/USD', {'values': ['EUR/USD', 'USD/JPY', 'USD/CHF', 'EUR/CHF', 'USD/CAD', 'NZD/USD',
'EUR/GBP', 'EUR/JPY', 'GBP/JPY', 'GBP/CHF']}],
'Période': ['Les derniers', {'values': ['Les premiers', 'Les derniers', 'Intervalle']}],
'Nombre(s) ou semaine(s)': ['100 000',
{'tip': "Les nombres supportent des espaces pour les séparateurs de milliers."
"\nLes semaines doivent être écrites comme ceci : '2016-14'."
"\n\tSi premiers → une semaine de fin."
"\n\tSi derniers → une semaine de début."
"\n\tSi intervalle → 2 semaines, séparées par une virgule."}],
'Tick': False, # à chaque variation du signal.
'Maille 1': False, # Renko, maille de 1 pip.
'Maille 2': False, # 2 pips.
'Maille 3': False, # 3 pips.
'Maille 4': False, # 4 pips.
'Maille 5': True, # 5 pips.
'Maille 7': False, # 7 pips.
'Maille 10': False, # 10 pips.
'Maille 14': False, # 14 pips.
'Maille 20': False, # 20 pips.
'1 mn': False, # 1 minute.
'5 mn': False, # 5 minutes.
'15 mn': False, # 15 minutes.
'30 mn': False, # 30 minutes.
'1 h': False, # 1 heure.
'4 h': False, # 4 heures.
'1 j': False, # 1 jour.
'1 s': False, # 1 semaine.
'* Voir historiques ': '', # '*' = bouton.
'* Télécharger ': '',
}
def my_signals(self, l_signals_in, num_socket_out):
l_signals = list()
for signal_key in self.fixed_params().keys():
b_signal = self.get_param(signal_key) # Booléen.
if isinstance(b_signal, bool) and b_signal:
typ_id, signal_ante, signal_now, signal_source = f'{self.type}{self.id}', '', signal_key, ''
l_signals.append((typ_id, signal_ante, signal_now, signal_source))
return l_signals
def get_state(self):
""" Surcharge. """
if self.b_chk:
for signal in self.fixed_params().keys():
if isinstance(self.get_param(signal), bool) and self.get_param(signal, False):
return True
return False
def refresh(self, l_keys):
if l_keys[1].strip().startswith('Voir'):
self.print_weeks()
elif l_keys[1].strip().startswith('Télécharger'):
self.download_weeks()
""" Option : après le téléchargement, on affiche l'état des téléchargements. """
self.print_weeks()
""" Propagation en cascade : Seulement sur les noms de signaux (type valeur = bool). """
if isinstance(self.get_param(l_keys[-1]), bool):
self.lo_sockets_out[0].to_update()
""" Infrastructure :Insertion de 'infrastructure' en 1ère place. """
l_keys = ['infrastructure'] + l_keys
super().refresh(l_keys)
def print_weeks(self):
""" Si Instrument == EUR/USD => Symbole == EURUSD """
hist = CtrlHistos(self.get_param('Instrument'))
hist.verify_weeks()
def download_weeks(self):
""" - Si des cases de ticks ou Rencko (Mailles) sont cochées => b_ticks
- Si des cases de candles sont cochées (1mn à 1m) => b_candles """
d_params = self.o_params.od_params[self.main_key]
b_ticks, b_candles = False, False
for key, val in self.fixed_params().items():
if isinstance(val, bool) and d_params[key]:
if key == 'Tick' or key.startswith('Maille'):
b_ticks = True
else:
b_candles = True
break
if not (b_ticks or b_candles):
print("Aucun historique n'a été sélectionné.")
return
hist = CtrlHistos(self.get_param('Instrument'))
if b_candles:
""" nb_weeks : adapter la valeur selon vos préférences. """
hist.download_histos(nb_weeks=5, b_ticks=False)
if b_ticks:
hist.download_histos(nb_weeks=1, b_ticks=True)
# class Calcul(CtrlCalcul):
# pass
nb_weeks
dans download_weeks()
.Remarque : la classe Calcul sera codée après l'implémentation de la base de données.
Bonjour les codeurs !