Improving our trading bot with trend analysis and OCO orders

in #hive-167922last year


versión en español disponible aquí

En mi post anterior, empezamos a construir un bot de trading automatico utilizando la API de Binance. Cubrimos la configuración inicial, incluyendo la configuración de la clave API y la importación de las bibliotecas necesarias. implementamos la lógica para el cálculo de medias móviles y la identificación de puntos de cruce. Ahora, vamos a continuar con más código y explorar funcionalidades adicionales.

Descargo de responsabilidad


La información proporcionada en este post es sólo para fines educativos y no debe interpretarse como asesoramiento de inversión. El trading automatizado conlleva un alto riesgo en el que se puede perder el capital invertido. Es importante hacer su propia investigación antes de tomar cualquier decisión de inversión.

Estructura de archivos


Para mantener una estructura modular, hemos dividido el código en tres archivos:

  1. binance_utils.py: Este archivo contiene funciones auxiliares utilizadas por el bot, incluyendo la consulta de balance, cálculo de cantidad, detección de cruce de las medias moviles, etc.
  2. 
    import numpy as np
    # Helper functions
    def get_balance(balances, asset):
        for bal in balances:
            if bal.get('asset') == asset:
                return float(bal['free'])
    def get_max_float_qty(step_size):
        max_float_qty = 0
        a = 10
        while step_size * a < 1:
          a = a*10**max_float_qty
          max_float_qty += 1
        return max_float_qty
    def get_quantity(price, amount, min_qty, max_qty, max_float_qty):
        quantity = amount / price
        if (quantity < min_qty or quantity > max_qty):
            return False
        quantity = np.round(quantity, max_float_qty)
        return quantity
    def crossover(ma_fast, ma_slow):
        if (ma_fast[0] < ma_slow[0] and ma_fast[1] >= ma_slow[1]):
            return True
        return False
    
  3. config.py: Este archivo almacena la API key y secrect, necesarios para acceder a la API de Binance.
  4. 
    API_KEY = "YOUR API KEY"
    API_SECRET = "YOUR API SECRET"
    
  5. bot.py: Este archivo contiene el código principal del bot. Se encarga de la sincronización de datos, inicializa el marco de datos, analiza los datos de las velas, realiza operaciones de trading y ejecuta el bucle infinito.

Importando Módulos

Tenemos que importar la librería binance.enums que nos permitirá manipular las órdenes OCO para poder colocar órdenes take profit y stop loss.

import time
import datetime as dt
import pandas as pd
import numpy as np
import pytz
from binance.client import Client
from binance.enums import *
import binance_utils as ut
from config import *

Definición de variables y parámetros


Esta vez añadimos una media móvil simple de 200 periodos para calcular la tendencia.


crypto = 'BTC'
ref = 'USDT'
symbol = crypto + ref
amount = 15
# Eligible periods for candlesticks 1m, 3m, 5m, 15m, 1h, etc.
period = '15m'
# Moving Average Periods
ema_f = 15
ema_s = 50
sma = 200

Sincronización de datos


Para asegurar un análisis preciso de los datos, empezamos por sincronizar la hora del bot con el servidor de Binance. La función synchronize() recupera los últimos datos de velas y calcula la hora de inicio del bucle del bot.


def synchronize():
    candles = client.get_klines(symbol=symbol,interval=period,limit=1)
    timer = pd.to_datetime(float(candles[0][0]) * 1000000)
    start = timer + dt.timedelta(minutes=step)
    print('Synchronizing .....')
    while dt.datetime.now(dt.timezone.utc) < pytz.UTC.localize(start):
        time.sleep(1)
    time.sleep(2)
    print('Bot synchronized')

Inicializando el Dataframe

La función initialize_dataframe() obtiene datos históricos de velas de Binance y crea un DataFrame de pandas.

def initialize_dataframe(limit):
    candles = client.get_klines(symbol=symbol,interval=period,limit=limit)
    df = pd.DataFrame(candles)
    df = df.drop([6, 7, 8, 9, 10, 11], axis=1)
    df.columns = ['time', 'open', 'high', 'low', 'close', 'volume']
    df[['time', 'open', 'high', 'low', 'close', 'volume']] = df[['time', 'open', 'high', 'low', 'close', 'volume']].astype(float)
    df['time'] = pd.to_datetime(df['time'] * 1000000)
    return df

Análisis de datos de velas

La función parser() inicializa el marco de datos y lo actualiza con los últimos datos de las velas. Calcula las EMAs, SMA, e identifica la tendencia actual basada en la relación del precio con el SMA.

def parser():
    df = initialize_dataframe(sma+1)
    df['ema_s'] = df['close'].ewm(span=ema_s).mean()
    df['ema_f'] = df['close'].ewm(span=ema_f).mean()
    df['sma'] = df['close'].rolling(window=sma).mean()
    df['trend'] = np.nan
    df['operation'] = np.nan
    # get trend
    if df['close'].iloc[-1] > df['sma'].iloc[-1]*1.005:
        trend = 'up'
    elif df['close'].iloc[-1] < df['sma'].iloc[-1]*0.995:
        trend = 'down'
    else:
        trend = None
    df['trend'].iloc[-1]= trend
    return df

Operaciones


La función operate() determina si se debe ejecutar una orden de compra o de venta en función del cruce de las EMAs y de la tendencia actual. Comprueba si hay órdenes abiertas para evitar órdenes duplicadas. Si se detecta una señal de operación, calcula la cantidad, comprueba el saldo disponible y coloca la orden limit adecuada. Además, establece órdenes take profit y stop loss para una mejor gestión del riesgo.


def operate(df):
    operation = None
    orders = client.get_open_orders(symbol=symbol)
    if len(orders) == 0:
        price = df['close'].iloc[-1]
        if ut.crossover(df.ema_f.values[-2:], df.ema_s.values[-2:]) and df['trend'].iloc[-1] == 'up':
            operation = 'BUY'
            quantity = ut.get_quantity(price, amount, min_qty, max_qty, max_float_qty)
            balances = client.get_account()['balances']
            balance = ut.get_balance(balances, ref)
            if not quantity:
                print('No Quantity available \n')
            elif balance <= amount:
                print(f'No {ref} to buy {crypto}')
            else:
                order = client.order_limit_buy(
                    symbol=symbol,
                    quantity=quantity,
                    price=price)
                status='NEW'
                while status != 'FILLED':
                    time.sleep(5)
                    order_id = order.get('orderId')
                    order = client.get_order(
                        symbol=symbol,
                        orderId= order_id)
                    status=order.get('status')
                sell_price = ((price * 1.02) // tick_size) * tick_size
                stop_price = ((price*0.991) // tick_size) * tick_size
                stop_limit_price = ((price*0.99) // tick_size) * tick_size
                order = client.order_oco_sell(
                    symbol=symbol,
                    quantity=quantity,
                    price=sell_price,
                    stopPrice=stop_price,
                    stopLimitPrice=stop_limit_price,
                    stopLimitTimeInForce='GTC')
        elif ut.crossover(df.ema_s.values[-2:], df.ema_f.values[-2:])and df['trend'].iloc[-1] == 'down':
            operation = 'SELL'
            quantity = ut.get_quantity(price, amount, min_qty, max_qty, max_float_qty)
            balances = client.get_account()['balances']
            balance = ut.get_balance(balances, crypto)
            if not quantity:
                print('No Quantity available \n')
            elif balance < quantity:
                print(f'No {crypto} to sell for {ref}')
            else:
                order = client.order_limit_sell(
                    symbol=symbol,
                    quantity=quantity,
                    price=price)
                status='NEW'
                while status != 'FILLED':
                    time.sleep(3)
                    order_id = order.get('orderId')
                    order = client.get_order(
                        symbol=symbol,
                        orderId= order_id)
                    status=order.get('status')
                buy_price = ((price*0.98) // tick_size) * tick_size
                stop_price = ((price*1.009) // tick_size) * tick_size
                stop_limit_price = ((price*1.01) // tick_size) * tick_size
                order = client.order_oco_buy(
                    symbol=symbol,
                    quantity=quantity,
                    price=buy_price,
                    stopPrice=stop_price,
                    stopLimitPrice=stop_limit_price,
                    stopLimitTimeInForce='GTC')
    return operation

Ejecución del bot


El bucle principal while en el bloque main del código obtiene y actualiza continuamente los datos. Añade los nuevos datos al marco de datos, ejecuta la operación comercial y guarda el marco de datos en un archivo CSV. Si el marco de datos supera un determinado tamaño, lo recorta para mantener la eficiencia. Si se produce alguna excepción, se registra en un archivo de errores con fines de depuración.


if __name__ == "__main__":
    pd.options.mode.chained_assignment = None
    client = Client(API_KEY, API_SECRET)
    info = client.get_symbol_info(symbol)
    min_qty = float(info['filters'][1].get('minQty'))
    step_size = float(info['filters'][1].get('stepSize'))
    max_qty = float(info['filters'][1].get('maxQty'))
    max_float_qty = ut.get_max_float_qty(step_size)
    tick_size = float(info['filters'][0].get('tickSize'))
    df = initialize_dataframe(1)
    step = int(period[:2]) if len(period) > 2 else int(period[0])
    if 'h' in period:
        step = step * 60
    synchronize()
    while True:
        temp = time.time()
        try:
            temp_df = parser()
            i=1
            while df['time'].iloc[-1] != temp_df['time'].iloc[-i] and i < sma:
                i+=1
            df = pd.concat([df, temp_df.tail(i-1)], ignore_index=True)
            df['operation'].iloc[-1] = operate(df.tail(2))
            if df.shape[0] > 10000:
                df = df.tail(10000)
            print(f"{df['time'].iloc[-1]} | Price:{df['close'].iloc[-1]} | Trend:{df['operation'].iloc[-1]} | Operation:{df['operation'].iloc[-1]}")
            df.to_csv(f'./{symbol}_{period}.csv')
        except Exception as e:
            with open('./error.txt', 'a') as file:
                file.write(f'Time = {dt.datetime.now(dt.timezone.utc)}\n')
                file.write(f'Error = {e}\n')
                file.write('----------------------\n')
        delay = time.time() - temp
        idle = 60 * step - delay
        if idle > 0:
            time.sleep(idle)
        else:
            synchronize()

Continuamos construyendo nuestro bot de trading utilizando la API de Binance. Implementamos funcionalidades para sincronizar datos, inicializar el marco de datos, analizar datos de velas y ejecutar operaciones de trading basadas en EMAs, SMAs y análisis de tendencias. También incorporamos la gestión de riesgos mediante órdenes de take profit / stop loss. La estructura modular del código permite un fácil mantenimiento y escalabilidad. En el próximo post, nos centraremos en las estrategias de backtesting y en la implementación de indicadores adicionales para mejorar el rendimiento del bot.

Recuerda manejar el trading automatizado con precaución y realizar pruebas exhaustivas antes de desplegar cualquier estrategia con fondos reales.

Puedes consultar todo este código en mi GitHub y si tienes alguna pregunta o sugerencia no dudes en dejar un comentario.

referencias: python-binance

In my previous post, we started building a cryptocurrency trading bot using the Binance API. We covered the initial setup, including API key configuration and importing necessary libraries. We implemented the logic for calculating moving averages and identifying crossover points. Now, let's continue with more code and explore additional functionalities.

Disclaimer


The information provided in this post is for educational purposes only and should not be construed as investment advice. Automated trading carries a high risk in which the invested capital can be lost. It is important to do your own research before making any investment decisions.

File Structure


To maintain a modular structure, we have divided the code into three files:

  1. binance_utils.py: This file contains utility functions used by the trading bot, including balance retrieval, quantity calculation, crossover detection, and more.
  2. 
    import numpy as np
    # Helper functions
    def get_balance(balances, asset):
        for bal in balances:
            if bal.get('asset') == asset:
                return float(bal['free'])
    def get_max_float_qty(step_size):
        max_float_qty = 0
        a = 10
        while step_size * a < 1:
          a = a*10**max_float_qty
          max_float_qty += 1
        return max_float_qty
    def get_quantity(price, amount, min_qty, max_qty, max_float_qty):
        quantity = amount / price
        if (quantity < min_qty or quantity > max_qty):
            return False
        quantity = np.round(quantity, max_float_qty)
        return quantity
    def crossover(ma_fast, ma_slow):
        if (ma_fast[0] < ma_slow[0] and ma_fast[1] >= ma_slow[1]):
            return True
        return False
    
  3. config.py: This file stores the API key and secret, which are required to access the Binance API.
  4. 
    API_KEY = "YOUR API KEY"
    API_SECRET = "YOUR API SECRET"
    
  5. bot.py: This file contains the main trading bot code. It handles data synchronization, initializes the dataframe, parses candlestick data, performs trading operations, and runs the bot loop.

Importing Modules


We have to import the library binance.enums that will allow us to manipulate the OCO orders so that we can place take profit and stop loss orders.


import time
import datetime as dt
import pandas as pd
import numpy as np
import pytz
from binance.client import Client
from binance.enums import *
import binance_utils as ut
from config import *

Defining Variables and Parameters


This time we added a 200-period simple moving average to calculate the trend.


crypto = 'BTC'
ref = 'USDT'
symbol = crypto + ref
amount = 15
# Eligible periods for candlesticks 1m, 3m, 5m, 15m, 1h, etc.
period = '15m'
# Moving Average Periods
ema_f = 15
ema_s = 50
sma = 200

Synchronizing Data


To ensure accurate data analysis, we start by synchronizing the bot's time with the Binance server. The synchronize() function retrieves the latest candlestick data and calculates the start time for the bot's loop.


def synchronize():
    candles = client.get_klines(symbol=symbol,interval=period,limit=1)
    timer = pd.to_datetime(float(candles[0][0]) * 1000000)
    start = timer + dt.timedelta(minutes=step)
    print('Synchronizing .....')
    while dt.datetime.now(dt.timezone.utc) < pytz.UTC.localize(start):
        time.sleep(1)
    time.sleep(2)
    print('Bot synchronized')

Initializing the Dataframe


The initialize_dataframe() function retrieves historical candlestick data from Binance and creates a pandas DataFrame.


def initialize_dataframe(limit):
    candles = client.get_klines(symbol=symbol,interval=period,limit=limit)
    df = pd.DataFrame(candles)
    df = df.drop([6, 7, 8, 9, 10, 11], axis=1)
    df.columns = ['time', 'open', 'high', 'low', 'close', 'volume']
    df[['time', 'open', 'high', 'low', 'close', 'volume']] = df[['time', 'open', 'high', 'low', 'close', 'volume']].astype(float)
    df['time'] = pd.to_datetime(df['time'] * 1000000)
    return df

Parsing Candlestick Data


The parser() function initializes the dataframe and updates it with the latest candlestick data. It calculates the EMA, SMA, and identifies the current trend based on the price's relationship with the SMA.


def parser():
    df = initialize_dataframe(sma+1)
    df['ema_s'] = df['close'].ewm(span=ema_s).mean()
    df['ema_f'] = df['close'].ewm(span=ema_f).mean()
    df['sma'] = df['close'].rolling(window=sma).mean()
    df['trend'] = np.nan
    df['operation'] = np.nan
    # get trend
    if df['close'].iloc[-1] > df['sma'].iloc[-1]*1.005:
        trend = 'up'
    elif df['close'].iloc[-1] < df['sma'].iloc[-1]*0.995:
        trend = 'down'
    else:
        trend = None
    df['trend'].iloc[-1]= trend
    return df

Trading Operations


The operate() function determines whether to execute a buy or sell order based on the crossover of the EMAs and the current trend. It checks for open orders to avoid duplicate orders. If a trade signal is detected, it calculates the quantity, checks the available balance, and places the appropriate limit order. Additionally, it sets up take profit and stop loss order for risk management.


def operate(df):
    operation = None
    orders = client.get_open_orders(symbol=symbol)
    if len(orders) == 0:
        price = df['close'].iloc[-1]
        if ut.crossover(df.ema_f.values[-2:], df.ema_s.values[-2:]) and df['trend'].iloc[-1] == 'up':
            operation = 'BUY'
            quantity = ut.get_quantity(price, amount, min_qty, max_qty, max_float_qty)
            balances = client.get_account()['balances']
            balance = ut.get_balance(balances, ref)
            if not quantity:
                print('No Quantity available \n')
            elif balance <= amount:
                print(f'No {ref} to buy {crypto}')
            else:
                order = client.order_limit_buy(
                    symbol=symbol,
                    quantity=quantity,
                    price=price)
                status='NEW'
                while status != 'FILLED':
                    time.sleep(5)
                    order_id = order.get('orderId')
                    order = client.get_order(
                        symbol=symbol,
                        orderId= order_id)
                    status=order.get('status')
                sell_price = ((price * 1.02) // tick_size) * tick_size
                stop_price = ((price*0.991) // tick_size) * tick_size
                stop_limit_price = ((price*0.99) // tick_size) * tick_size
                order = client.order_oco_sell(
                    symbol=symbol,
                    quantity=quantity,
                    price=sell_price,
                    stopPrice=stop_price,
                    stopLimitPrice=stop_limit_price,
                    stopLimitTimeInForce='GTC')
        elif ut.crossover(df.ema_s.values[-2:], df.ema_f.values[-2:])and df['trend'].iloc[-1] == 'down':
            operation = 'SELL'
            quantity = ut.get_quantity(price, amount, min_qty, max_qty, max_float_qty)
            balances = client.get_account()['balances']
            balance = ut.get_balance(balances, crypto)
            if not quantity:
                print('No Quantity available \n')
            elif balance < quantity:
                print(f'No {crypto} to sell for {ref}')
            else:
                order = client.order_limit_sell(
                    symbol=symbol,
                    quantity=quantity,
                    price=price)
                status='NEW'
                while status != 'FILLED':
                    time.sleep(3)
                    order_id = order.get('orderId')
                    order = client.get_order(
                        symbol=symbol,
                        orderId= order_id)
                    status=order.get('status')
                buy_price = ((price*0.98) // tick_size) * tick_size
                stop_price = ((price*1.009) // tick_size) * tick_size
                stop_limit_price = ((price*1.01) // tick_size) * tick_size
                order = client.order_oco_buy(
                    symbol=symbol,
                    quantity=quantity,
                    price=buy_price,
                    stopPrice=stop_price,
                    stopLimitPrice=stop_limit_price,
                    stopLimitTimeInForce='GTC')
    return operation

Bot Execution


The main while loop in the main block of the code continuously fetches and updates the data. It appends the new data to the dataframe, executes the trading operation, and saves the dataframe to a CSV file. If the dataframe exceeds a certain size, it trims it to maintain efficiency. If any exceptions occur, they are logged in an error file for debugging purposes.


if __name__ == "__main__":
    pd.options.mode.chained_assignment = None
    client = Client(API_KEY, API_SECRET)
    info = client.get_symbol_info(symbol)
    min_qty = float(info['filters'][1].get('minQty'))
    step_size = float(info['filters'][1].get('stepSize'))
    max_qty = float(info['filters'][1].get('maxQty'))
    max_float_qty = ut.get_max_float_qty(step_size)
    tick_size = float(info['filters'][0].get('tickSize'))
    df = initialize_dataframe(1)
    step = int(period[:2]) if len(period) > 2 else int(period[0])
    if 'h' in period:
        step = step * 60
    synchronize()
    while True:
        temp = time.time()
        try:
            temp_df = parser()
            i=1
            while df['time'].iloc[-1] != temp_df['time'].iloc[-i] and i < sma:
                i+=1
            df = pd.concat([df, temp_df.tail(i-1)], ignore_index=True)
            df['operation'].iloc[-1] = operate(df.tail(2))
            if df.shape[0] > 10000:
                df = df.tail(10000)
            print(f"{df['time'].iloc[-1]} | Price:{df['close'].iloc[-1]} | Trend:{df['operation'].iloc[-1]} | Operation:{df['operation'].iloc[-1]}")
            df.to_csv(f'./{symbol}_{period}.csv')
        except Exception as e:
            with open('./error.txt', 'a') as file:
                file.write(f'Time = {dt.datetime.now(dt.timezone.utc)}\n')
                file.write(f'Error = {e}\n')
                file.write('----------------------\n')
        delay = time.time() - temp
        idle = 60 * step - delay
        if idle > 0:
            time.sleep(idle)
        else:
            synchronize()

We continued building our cryptocurrency trading bot using the Binance API. We implemented functionalities to synchronize data, initialize the dataframe, parse candlestick data, and execute trading operations based on EMAs, SMAs, and trend analysis. We also incorporated risk management through stop loss orders. The modular structure of the code allows for easy maintenance and scalability. In the next post, we will focus on backtesting strategies and implementing additional indicators to enhance the bot's performance.

Remember to handle automated trading with caution and perform thorough testing before deploying any strategies with real funds.

You can check all this code on my GitHub and if you have any questions or suggestions please feel free to leave a comment.


references: python-binance