Trading automatique FX avec algorithme génétique Partie 3 Trading réel avec l'API Oanda

Article précédent

Système de trading automatique FX réalisé avec python et algorithme génétique Partie 1 Trading automatique FX avec algorithme génétique Partie 2 Implémentation de l'IA de trading en évolution

Choses à faire cette fois

Je voudrais écrire sur l'API Oanda que j'ai utilisée lorsque j'ai créé le système de trading automatique et la fonction pour passer des ordres d'achat et de vente.

API Oanda utilisée pour le trading automatique

Il a été construit en utilisant 5 types d'API. Pour plus de détails, voir Document API d'Oanda Le jeton OAuth2 peut être émis sur le Web en ouvrant un compte et en vous connectant. Définissez le jeton sur Authorization: Bearer ******** sur la commande curl Header.

"""
01.API d'informations de compte
curl -H "Authorization: Bearer ********" https://api-
fxtrade.oanda.com/v1/accounts/12345
"""
{
    "accountId" : 12345,
    "accountName" : "Primary",
    "balance" : 123456.5765,
    "unrealizedPl" : 36.8816,
    "realizedPl" : 235.5839,
    "marginUsed" : 123456.154,
    "marginAvail" : 123456.3041,
    "openTrades" : 20,
    "openOrders" : 0,
    "marginRate" : 0.04,
    "accountCurrency" : "JPY"
}


"""
02.Commande API
curl -H "Authorization: Bearer ********" -X POST -d "instrument=EUR_USD&units=2&side=sell&type=market" "https://api-fxtrade.oanda.com/v1/accounts/12345/orders"
"""
{

  "instrument" : "EUR_USD",
  "time" : "2013-12-06T20:36:06Z", // Time that order was executed
  "price" : 1.37041,               // Trigger price of the order
  "tradeOpened" : {
    "id" : 175517237,              // Order id
    "units" : 1000,                // Number of units
    "side" : "buy",                // Direction of the order
    "takeProfit" : 0,              // The take-profit associated with the Order, if any
    "stopLoss" : 0,                // The stop-loss associated with the Order, if any
    "trailingStop" : 0             // The trailing stop associated with the rrder, if any
  },
  "tradesClosed" : [],
  "tradeReduced" : {}
}

"""
03.Obtenir la liste des positions actuelles
curl -H "Authorization: Bearer ####" https://api-fxtrade.oanda.com/v1/accounts/######/positions
"""
{
    "positions" : [
        {
            "instrument" : "AUD_USD",
            "units" : 100,
            "side" : "sell",
            "avgPrice" : 0.78216
        },
        {
            "instrument" : "GBP_USD",
            "units" : 100,
            "side" : "sell",
            "avgPrice" : 1.49128
        },
        {
            "instrument" : "USD_JPY",
            "units" : 850,
            "side" : "buy",
            "avgPrice" : 119.221
        }
}

"""
04.Obtenez le taux du marché
curl -H "Authorization: Bearer ********" -X GET "https://api-fxtrade.oanda.com/v1/prices?instruments=EUR_USD%2CUSD_JPY%2CEUR_CAD"
"""
{
    "prices" : [
        {
            "instrument" : "EUR_USD",
            "time" : "2015-03-13T20:59:58.165668Z",
            "bid" : 1.04927,
            "ask" : 1.04993,
            "status" : "halted"
        },
        {
            "instrument" : "USD_JPY",
            "time" : "2015-03-13T20:59:58.167818Z",
            "bid" : 121.336,
            "ask" : 121.433,
            "status" : "halted"
        },
        {
            "instrument" : "EUR_CAD",
            "time" : "2015-03-13T20:59:58.165465Z",
            "bid" : 1.34099,
            "ask" : 1.34225,
            "status" : "halted"
        }
    ]
}

"""
05.API pour vérifier l'état des commandes d'achat et de vente
curl -H "Authorization: Bearer ************" https://api-fxtrade.oanda.com/v1/accounts/****/transactions
"""
{
     "id" : 1789536248,
     "accountId" : *****,
     "time" : "2015-03-20T12:17:06.000000Z",
     "type" : "TAKE_PROFIT_FILLED",
     "tradeId" : 1789531428,
     "instrument" : "USD_JPY",
     "units" : 1,
     "side" : "sell",
     "price" : 121.017,
     "pl" : 0.02,
     "interest" : 0,
     "accountBalance" : 33299999.1173
},

          {
     "id" : 1789560748,
     "accountId" : *****,
     "time" : "2015-03-20T12:49:38.000000Z",
     "type" : "STOP_LOSS_FILLED",
     "tradeId" : 1789500620,
     "instrument" : "USD_JPY",
     "units" : 100,
     "side" : "sell",
     "price" : 120.899,
     "pl" : -18.6,
     "interest" : 0,
     "accountBalance" : 33299980.3023
}


Exemple de code pour accéder à l'API

Ceci est un exemple de mise en œuvre de l'API d'acquisition de prix. Accès Web avec requête urllib. La réponse sera renvoyée dans Json, donc je ferai de mon mieux pour écrire la classe parser. Lorsque le spread s'ouvre (le marché devient rugueux et la différence entre le prix d'achat et le prix de vente s'ouvre), j'ai essayé d'incorporer une fonction qui ne passe pas d'ordre lorsque le taux est défavorable.

■ Exemple de code accédant à l'API sandbox http://api-sandbox.oanda.com/v1/prices?instruments=EUR_USD%2CUSD_JPY%2CGBP_USD%2CAUD_USD

price_api.py


# -*- coding: utf-8 -*-
from __future__ import absolute_import
from __future__ import unicode_literals
import urllib
import requests
import time
import ujson
import pytz
from enum import Enum


class CurrencyPair(Enum):
    EUR_USD = 1
    USD_JPY = 2
    GBP_USD = 3
    AUD_USD = 4


class OandaAPIMode(Enum):
    PRODUCTION = 1
    DEVELOP = 2
    SANDBOX = 3
    DUMMY = 4

    @property
    def url_base(self):
        api_base_url_dict = {
            OandaAPIMode.PRODUCTION: 'https://api-fxtrade.oanda.com/',
            OandaAPIMode.DEVELOP: 'https://api-fxpractice.oanda.com/',
            OandaAPIMode.SANDBOX: 'http://api-sandbox.oanda.com/',
        }
        return api_base_url_dict.get(self)

    @property
    def headers(self):
        """
        :rtype : dict
        """
        _base = {
            'Accept-Encoding': 'identity, deflate, compress, gzip',
            'Accept': '*/*', 'User-Agent': 'python-requests/1.2.0',
            'Content-type': 'application/x-www-form-urlencoded',
        }

        if self == OandaAPIMode.SANDBOX:
            return _base
        if self == OandaAPIMode.DEVELOP:
            _base['Authorization'] = 'Bearer {}'.format(get_password('OandaRestAPITokenDemo'))
            return _base
        if self == OandaAPIMode.PRODUCTION:
            _base['Authorization'] = 'Bearer {}'.format(get_password('OandaRestAPIToken'))
            return _base
        raise ValueError


class OandaServiceUnavailableError(Exception):
    """
Erreur lors de l'arrêt du service
    """
    pass


class OandaInternalServerError(Exception):
    """
Erreur lors de l'arrêt du service
    """
    pass


class OandaAPIBase(object):
    mode = None

    class Meta(object):
        abstract = True

    def __init__(self, mode):
        """
        :param mode: OandaAPIMode
        """
        self.mode = mode

    def requests_api(self, url, payload=None):
        #Si la communication échoue, réessayez jusqu'à 3 fois
        for x in xrange(3):
            response = None
            try:
                if payload:
                    response = requests.post(url, headers=self.mode.headers, data=payload)
                else:
                    response = requests.get(url, headers=self.mode.headers)

                assert response.status_code == 200, response.status_code
                print 'API_Access: {}'.format(url)
                data = ujson.loads(response.text)
                self.check_json(data)
                return data
            except Exception as e:
                if response is None:
                    raise
                if response.text:
                    if str("Service Unavailable") in str(response.text):
                        raise OandaServiceUnavailableError
                    if str("An internal server error occurred") in str(response.text):
                        raise OandaInternalServerError
                time.sleep(3)
                if x >= 2:
                    raise ValueError, response.text
        raise

    def check_json(self, data):
        raise NotImplementedError


class PriceAPI(OandaAPIBase):
    """
API de confirmation de tarif

    %2C est une virgule

    curl -H "Authorization: Bearer ********" -X GET "https://api-fxtrade.oanda.com/v1/prices?instruments=EUR_USD%2CUSD_JPY%2CEUR_CAD"

    {
        "prices" : [
            {
                "instrument" : "EUR_USD",
                "time" : "2015-03-13T20:59:58.165668Z",
                "bid" : 1.04927,
                "ask" : 1.04993,
                "status" : "halted"
            },
            {
                "instrument" : "USD_JPY",
                "time" : "2015-03-13T20:59:58.167818Z",
                "bid" : 121.336,
                "ask" : 121.433,
                "status" : "halted"
            },
            {
                "instrument" : "EUR_CAD",
                "time" : "2015-03-13T20:59:58.165465Z",
                "bid" : 1.34099,
                "ask" : 1.34225,
                "status" : "halted"
            }
        ]
    }

    :rtype : dict of PriceAPIModel
    """
    url_base = '{}v1/prices?'

    def get_all(self):
        instruments = ','.join([x.name for x in CurrencyPair])
        instruments = urllib.quote(instruments, '')
        url = self.url_base.format(self.mode.url_base)
        url += 'instruments={}'.format(instruments)
        data = self.requests_api(url)
        d = {}
        for price in data.get('prices'):
            price_model = PriceAPIModel(price)
            d[price_model.currency_pair] = price_model
        return d

    def check_json(self, data):
        assert 'prices' in data


class PriceAPIModel(object):
    """
Classe qui analyse l'API de confirmation de taux
    """
    instrument = None
    time = None
    bid = None  #Cochez au moment de VENDRE
    ask = None  #Cochez au moment de ACHETER
    status = None

    def __init__(self, price):
        self.ask = float(price.get('ask'))
        self.bid = float(price.get('bid'))
        self.time = parse_time(price.get('time'))
        self.instrument = str(price.get('instrument'))
        if 'status' in price:
            self.status = str(price.get('status'))
        self._check(price)

    def _check(self, price):
        if 'ask' not in price:
            raise ValueError
        if 'bid' not in price:
            raise ValueError
        if 'time' not in price:
            raise ValueError
        if 'instrument' not in price:
            raise ValueError
        self.currency_pair

    @property
    def currency_pair(self):
        """
        :rtype : CurrencyPair
        """
        for pair in CurrencyPair:
            if str(pair.name) == str(self.instrument):
                return pair
        raise ValueError

    @property
    def is_maintenance(self):
        """
Vrai si pendant la maintenance
        :rtype : bool
        """
        if self.status is None:
            return False
        if str(self.status) == str('halted'):
            return True
        return False

    def is_active(self):
        """
Vrai si tarif valide
        :rtype : bool
        """
        #Pas en maintenance
        if self.is_maintenance:
            return False

        #Permet jusqu'à 4 ticks taux normal
        if self.cost_tick > 4:
            return False
        return True


def get_password(key):
    """
Répondre mot de passe
    """
    return "12345"


def parse_time(time_text):
    """
Convertissez une chaîne en UTC.
Exemple)
    2015-02-22T15:00:00.000000Z
    :param time_text: char
    :rtype : datetime
    """
    import datetime
    t = time_text
    utc = datetime.datetime(int(t[0:4]),
                            int(t[5:7]),
                            int(t[8:10]),
                            int(t[11:13]),
                            int(t[14:16]),
                            int(t[17:19]),
                            int(t[20:26]), tzinfo=pytz.utc)
    return utc


def utc_to_jst(utc):
    return utc.astimezone(pytz.timezone('Asia/Tokyo'))


print "START"
# API_Access
price_group_dict = PriceAPI(OandaAPIMode.SANDBOX).get_all()

#Imprimer le résultat
for key in price_group_dict:
    pair = key
    price = price_group_dict[key]
    print "Pair:{} bid:{} ask:{} time:{}".format(pair.name, price.bid, price.ask, price.time)
print "FINISH"

Résultat d'exécution


>python price_api.py 
START
API_Access: http://api-sandbox.oanda.com/v1/prices?instruments=EUR_USD%2CUSD_JPY%2CGBP_USD%2CAUD_USD
Pair:USD_JPY bid:120.243 ask:120.293 time:2015-09-23 21:07:30.363800+00:00
Pair:AUD_USD bid:0.70049 ask:0.70066 time:2015-09-23 21:07:30.367431+00:00
Pair:EUR_USD bid:1.13916 ask:1.1393 time:2015-10-13 07:16:53.931425+00:00
Pair:GBP_USD bid:1.52455 ask:1.5248 time:2015-09-23 21:07:30.362069+00:00
FINISH

Des erreurs inattendues se produisent souvent dans les systèmes de production. Puisque l'API Oanda est simple, je pense qu'il sera plus facile d'écrire la bibliothèque vous-même plus tard. Si vous souhaitez utiliser la bibliothèque, vous devez utiliser la bibliothèque en considérant que la réponse de l'API Saturday-Sunday s'arrêtera.

Un cauchemar quand 100 IA sont achetées et vendues (problème à deux étages)

Lorsque 100 AI achète et vend des yens en dollars au moment où la barre de 5 minutes est mise à jour, si l'achat est de 60 et la vente est de 40, les 40 pièces qui se chevauchent seront à deux étages et seront perdues dans les frais. Pour contrer le problème du double étage, nous optimiserons l'achat et la vente en gros.

■ Exemple de cas Minimisez les frais de négociation lorsque AI1-6 passe un ordre comme suit. On suppose que les frais de transaction sont de 100 yens.

AI Name Order bid Limit StopLimit
AI.001 Buy 120.00 120.50 119.50
AI.002 Buy 120.00 120.70 119.50
AI.003 Buy 120.00 120.50 119.50
AI.004 Buy 120.00 120.50 119.00
AI.005 Sell 120.00 119.00 120.50
AI.006 Sell 120.00 118.50 120.50

■ Méthode 1. À 120,00 yens, passez une commande pour 4 achats et 2 ventes (frais de négociation 600 yens) Après cela, le prix du marché fluctue et les 6 ordres sont épuisés (frais de négociation 600 yens) 1200 yens au total

■ Méthode 2. À 120,00 yens, regroupez les commandes pour AI1 à 6 et passez une commande pour 2 dollars yens 120,00 Acheter (frais d'achat et de vente 200 yens) Après cela, le prix du marché fluctue et les 6 ordres sont épuisés (frais de négociation 600 yens) 800 yens au total

Oanda est un système à deux étages qui n'a pas de position, j'ai donc décidé d'acheter et de vendre en utilisant la solution 2.

Si vous utilisez la méthode de commande groupée pour les ordres d'achat et de vente, il y aura un effet secondaire que vous ne serez pas en mesure de placer des ordres Limit (profit) et Stop Limit (perte à limite)

Dans la méthode de la solution 1, vous pouvez définir la limite et la limite d'arrêt pour chaque commande et passer une commande, donc une fois que vous passez la commande, Oanda réglera automatiquement le paiement, mais si vous utilisez la méthode de la solution 2, l'unité de commande sera différente, alors commandez Vous devez également passer une commande de paiement à partir de votre propre programme.

Introduisez Supervisord pour augmenter la disponibilité

Que faire si le programme de paiement s'arrête avec une erreur? Le système continue de passer des commandes, mais la position n'est pas clôturée. L'effet de levier des comptes continue d'augmenter. Pour éviter que cela ne se produise, le programme d'achat et de vente doit être hautement disponible. J'ai également installé Fonction de notification d'appel lorsqu'une erreur se produit.

Pour atteindre une haute disponibilité, il est facile de traiter le programme avec superviseur. Créez des commandes d'ouverture et de fermeture pour les positions (ordres d'achat et de vente) en Python et utilisez Supervisord pour lancer le processus. Des fonctions similaires peuvent être obtenues avec Cron et while True, mais elles présentent de nombreux inconvénients et n'ont pas été adoptées.

méthode Démérite
cron La réexécution immédiate n'est pas possible après la fin de la commande
while True Une fuite de mémoire se produit, l'erreur ne peut pas être automatiquement récupérée lorsqu'elle est supprimée
supervisord Tracas à installer, tracas à déployer

order_open.py


# -*- coding: utf-8 -*-
#C'est un pseudo-langage parce qu'il est habilité. Peut-être que ça ne marche pas. Pardon
from __future__ import absolute_import
from __future__ import unicode_literals
import datetime
from django.core.management import BaseCommand
import time
import sys
import pytz
from requests import ConnectionError

ACCOUNT_ID = 12345


class CustomBaseCommand(BaseCommand):
    class Meta(object):
        abstract = True

    def echo(self, txt):
        d = datetime.datetime.today()
        print d.strftime("%Y-%m-%d %H:%M:%S") + ':' + txt


class Command(CustomBaseCommand):
    """
Passer une commande avec BoardAI
    """
    CACHE_PREV_RATES = {}

    def handle(self, *args, **options):
        try:
            self.run()
        except OandaServiceUnavailableError:
            #Pendant la maintenance du samedi et du dimanche
            self.echo("ServiceUnavailableError")
            time.sleep(60)
        except OandaInternalServerError:
            #Pendant la maintenance du samedi et du dimanche
            self.echo("OandaInternalServerError")
            time.sleep(60)
        except ConnectionError:
            time.sleep(60)

        #Arrêtez-vous pendant 3 secondes
        time.sleep(3)

    def run(self):
        #Vérification de la marge
        account = AccountAPI(OandaAPIMode.PRODUCTION, ACCOUNT_ID).get_all()
        if int(account.get('marginAvail')) < 10000:
            self.echo('MARGIN EMPTY!! marginAvail:{}'.format(int(account.get('marginAvail'))))
            time.sleep(3000)

        #prendre le prix
        price_group = PriceAPI(OandaAPIMode.PRODUCTION).get_all()

        #Génération d'instances IA
        order_group = []
        ai_board_group = AIBoard.get_all()

        #Commande temporaire
        now = datetime.datetime.now(tz=pytz.utc)
        for ai_board in ai_board_group:
            order = self.pre_order(ai_board, price_group, now)
            if order:
                order_group.append(order)

        #Commande
        self.order(order_group, price_group)

    def pre_order(self, ai_board, price_group, now):
        """
Retourner True après la commande
        :param ai_board: AIBoard
        :param price_group: dict of PriceAPIModel
        :param now: datetime
        :rtype : bool
        """
        ai = ai_board.get_ai_instance()
        price = price_group.get(ai.currency_pair, None)

        #Le prix est-il normal?
        if price is None:
            return None
        if not price.is_active():
            # print 'price not active'
            return None

        #Le taux n'est pas normal
        prev_rates = self.get_prev_rates(ai.currency_pair, Granularity.H1)
        if not prev_rates:
            return None
        prev_rate = prev_rates[-1]

        #Écart par rapport au taux précédent dans les 3 minutes
        if now - prev_rate.start_at > datetime.timedelta(seconds=60 * 3):
            # print 'ORDER TIME IS OVER'
            return None

        #Limite d'achat par nombre de positions et limite d'achat par temps
        if not ai_board.can_order(prev_rate):
            # print 'TIME OR POSITION LIMIT'
            return None

        #Décision d'achat Similaire à la simulation d'IA, l'achat et la vente ne
        order_ai = ai.get_order_ai(prev_rates, price.mid, price.time, is_production=True)

        if order_ai is None:
            return None
        if order_ai.order_type == OrderType.WAIT:
            return None

        #Tir d'ordre temporaire
        order = Order.pre_open(ai_board, order_ai, price, prev_rate.start_at)
        return order

    def order(self, order_group, price_group):
        """
Prenez un récapitulatif de la précommande et passez effectivement la commande
        :param order_group: list of Order
        :param price_group: dict of PriceAPIModel
        :return:
        """
        #Prenez un résumé
        order_dict = {x: 0 for x in CurrencyPair}
        for order in order_group:
            if order.buy:
                order_dict[order.currency_pair] += order.units
            else:
                order_dict[order.currency_pair] -= order.units
        print order_dict

        #Commande
        api_response_dict = {}
        for key in order_dict:
            if order_dict[key] == 0:
                print '{}:NO ORDER'.format(key)
                continue
            print '{}:ORDER!'.format(key)
            units = order_dict[key]
            api_response_dict[key] = OrdersAPI(OandaAPIMode.PRODUCTION, ACCOUNT_ID).post(key, units, tag='open')

        #Mise à jour de la base de données
        for order in order_group:
            order.open(price_group.get(order.currency_pair))

    def get_prev_rates(self, currency_pair, granularity):
        key = self._get_key(currency_pair, granularity)
        print key
        r = self.CACHE_PREV_RATES.get(key)
        if r:
            return r
        prev_rates = CurrencyPairToTable.get_table(currency_pair, granularity).get_new_record_by_count(10000)
        self.CACHE_PREV_RATES[key] = prev_rates
        return prev_rates

    def _get_key(self, currency_pair, granularity):
        return 'RATE:{}:{}'.format(currency_pair.value, granularity.value)

order_close.py


# -*- coding: utf-8 -*-
#C'est un pseudo-langage parce qu'il est habilité. Peut-être que ça ne marche pas. Pardon
from __future__ import absolute_import
from __future__ import unicode_literals
import datetime
from django.core.management import BaseCommand
import time
import pytz
from requests import ConnectionError

class Command(CustomBaseCommand):
    """
Fermer à l'aide de l'historique du compte
    """
    CACHE_PREV_RATES = {}

    def handle(self, *args, **options):
        print '********************************'
        self.echo('close start')
        self.check_kill_switch()

        try:
            self.run()
        except OandaServiceUnavailableError:
            #Pendant la maintenance du samedi et du dimanche
            self.echo("ServiceUnavailableError")
            time.sleep(60)
        except OandaInternalServerError:
            #Pendant la maintenance du samedi et du dimanche
            self.echo("OandaInternalServerError")
            time.sleep(60)
        except ConnectionError:
            time.sleep(60)
        except Exception as e:
            self.critical_error('close', e)
        time.sleep(3)

    def run(self):
        orders = Order.get_open()

        #prendre le prix
        price_group = PriceAPI(OandaAPIMode.PRODUCTION).get_all()

        #Prenez un résumé
        order_dict = {x: 0 for x in CurrencyPair}
        for order in orders:
            if order.can_close(price_group[order.currency_pair]):
                if order.buy:
                    #Achat et vente inversés
                    print order.currency_pair, order.units
                    order_dict[order.currency_pair] -= order.units
                    print order_dict[order.currency_pair]
                else:
                    #Achat et vente inversés
                    print order.currency_pair, order.units
                    order_dict[order.currency_pair] += order.units
                    print order_dict[order.currency_pair]
        print order_dict

        #Commande
        api_response_dict = {}
        for key in order_dict:
            if order_dict[key] == 0:
                print '{}:NO ORDER'.format(key)
                continue
            print '{}:ORDER'.format(key)
            units = order_dict[key]
            api_response_dict[key] = OrdersAPI(OandaAPIMode.PRODUCTION, 12345).post(key, units, tag='close')

        #Record
        for order in orders:
            if order.can_close(price_group[order.currency_pair]):
                order.close(price_group.get(order.currency_pair))

Construire un cluster informatique d'algorithmes génétiques

J'ai essayé de construire un cluster informatique d'algorithmes génétiques avec mes spécifications. L'ouvrier fait une commande pour calculer avec un algorithme génétique en python et le lance avec supervisord. スクリーンショット 2015-10-13 17.37.46.png

Netabare: historique des transactions

Avez-vous fait un profit du trading automatique après tout?

Mois Bénéfice commercial Frais de maintenance du serveur
Mars 2015 +50 000 10 000
Avril 2015 +20 000 10 000
Mai 2015 -70 000 10 000
Juin 2015 -50 000 10 000
Juillet 2015 Arrêtez le système de trading automatique et l'ETF:Montant total en 1557 -

Merci de rester avec nous jusqu'à la fin. Le développement du système de trading automatique était très intéressant. Essayez-le.

Article associé

Système de trading automatique FX réalisé avec python et algorithme génétique Partie 1 Trading automatique FX avec algorithme génétique Partie 2 Implémentation de l'IA de trading en évolution Trading automatique FX avec algorithme génétique Partie 3 Trading réel avec l'API Oanda

Recommended Posts

Trading automatique FX avec algorithme génétique Partie 3 Trading réel avec l'API Oanda
Trading automatique FX avec algorithme génétique Partie 2 Mise en œuvre d'une IA de trading en évolution
Système de trading automatique FX réalisé avec python et algorithme génétique Partie 1
Trouvez la valeur optimale de la fonction à l'aide d'un algorithme génétique (partie 2)
Utilisez Nutanix avec l'API REST, partie 2
Les débutants créeront un bot de trading automatique Bitcoin visant beaucoup d'argent! Partie 2 [Transaction avec API]
Suivi automatique à l'aide de l'API de streaming avec Tweepy