intelligent-trading-bot/outputs/trader_binance.py

411 lines
14 KiB
Python
Raw Permalink Normal View History

2020-02-23 20:45:50 +01:00
import os
import sys
import argparse
import math, time
from datetime import datetime
2020-12-13 15:06:24 +01:00
from decimal import *
2020-02-23 20:45:50 +01:00
import pandas as pd
import asyncio
2024-12-15 15:25:11 +01:00
from binance import Client
2020-02-23 20:45:50 +01:00
from binance.exceptions import *
from binance.helpers import date_to_milliseconds, interval_to_milliseconds
2020-03-08 21:09:08 +01:00
from binance.enums import *
2020-02-23 20:45:50 +01:00
2021-09-04 18:19:04 +02:00
from service.App import *
2021-09-04 21:28:45 +02:00
from common.utils import *
from outputs.notifier_trades import get_signal
2020-02-23 20:45:50 +01:00
import logging
2021-09-04 21:28:45 +02:00
log = logging.getLogger('trader')
2020-12-13 15:06:24 +01:00
2021-01-01 13:38:07 +01:00
async def trader_binance(df, model: dict, config: dict):
2021-01-01 13:38:07 +01:00
"""
It is a highest level task which is added to the event loop and executed normally every 1 minute and then it calls other tasks.
"""
symbol = config["symbol"]
freq = config["freq"]
startTime, endTime = pandas_get_interval(freq)
2021-01-01 13:38:07 +01:00
now_ts = now_timestamp()
2025-02-16 12:47:07 +01:00
buy_signal_column = model.get("buy_signal_column")
sell_signal_column = model.get("sell_signal_column")
2024-06-22 09:58:08 +02:00
2025-02-16 12:47:07 +01:00
signal = get_signal(buy_signal_column, sell_signal_column)
2024-06-22 09:58:08 +02:00
signal_side = signal.get("side")
close_price = signal.get("close_price")
close_time = signal.get("close_time")
2021-01-01 13:38:07 +01:00
log.info(f"===> Start trade task. Timestamp {now_ts}. Interval [{startTime},{endTime}].")
2021-01-01 13:38:07 +01:00
#
# Sync trade status, check running orders (orders, account etc.)
#
status = App.status
2021-01-01 13:38:07 +01:00
if status == "BUYING" or status == "SELLING":
# We expect that an order was created before and now we need to check if it still exists or was executed
# -----
order_status = await update_order_status()
order = App.order
2021-01-01 13:38:07 +01:00
# If order status executed then change the status
# Status codes: NEW PARTIALLY_FILLED FILLED CANCELED PENDING_CANCEL(currently unused) REJECTED EXPIRED
if not order or not order_status:
# No sell order exists or some problem
# TODO: Recover, reset, init/sync state (cannot trade because wrong happened with the order or connection or whatever)
# check connection (like ping), then server status, then our own account status, then funds, orders etc.
# -----
await update_trade_status()
log.error(f"Bad order or order status {order}. Full reset/init needed.")
return
if order_status == ORDER_STATUS_FILLED:
2021-03-07 10:44:32 +01:00
log.info(f"Limit order filled. {order}")
2021-01-01 13:38:07 +01:00
if status == "BUYING":
2021-03-07 10:44:32 +01:00
print(f"===> BOUGHT: {order}")
App.status = "BOUGHT"
2021-01-01 13:38:07 +01:00
elif status == "SELLING":
2021-03-07 10:44:32 +01:00
print(f"<=== SOLD: {order}")
App.status = "SOLD"
log.info(f'New trade mode: {App.status}')
2021-01-01 13:38:07 +01:00
elif order_status == ORDER_STATUS_REJECTED or order_status == ORDER_STATUS_EXPIRED or order_status == ORDER_STATUS_CANCELED:
log.error(f"Failed to fill order with order status {order_status}")
if status == "BUYING":
App.status = "SOLD"
2021-01-01 13:38:07 +01:00
elif status == "SELLING":
App.status = "BOUGHT"
log.info(f'New trade mode: {App.status}')
2021-01-01 13:38:07 +01:00
elif order_status == ORDER_STATUS_PENDING_CANCEL:
return # Currently do nothing. Check next time.
elif order_status == ORDER_STATUS_PARTIALLY_FILLED:
pass # Currently do nothing. Check next time.
elif order_status == ORDER_STATUS_NEW:
pass # Wait further for execution
else:
pass # Order still exists and is active
elif status == "BOUGHT" or status == "SOLD":
pass # Do nothing
else:
log.error(f"Wrong status value {status}.")
#
# Prepare. Kill or update existing orders (if necessary)
#
status = App.status
2021-01-01 13:38:07 +01:00
# If not sold for 1 minute, then kill and then a new order will be created below if there is signal
# Essentially, this will mean price adjustment (if a new order of the same direction will be created)
# In future, we might kill only after some timeout
if status == "BUYING" or status == "SELLING": # Still not sold for 1 minute
# -----
2021-03-07 10:44:32 +01:00
order_status = await cancel_order()
if not order_status:
# Cancel exception (the order still exists) or the order was filled and does not exist
await update_trade_status()
return
await asyncio.sleep(1) # Wait for a second till the balance is updated
2021-01-01 13:38:07 +01:00
if status == "BUYING":
App.status = "SOLD"
2021-01-01 13:38:07 +01:00
elif status == "SELLING":
App.status = "BOUGHT"
2021-01-01 13:38:07 +01:00
#
# Trade by creating orders
#
status = App.status
2024-06-22 09:58:08 +02:00
2021-03-07 10:44:32 +01:00
if signal_side == "BUY":
print(f"===> BUY SIGNAL {signal}: ")
elif signal_side == "SELL":
print(f"<=== SELL SIGNAL: {signal}")
2021-01-01 13:38:07 +01:00
else:
2024-06-22 09:58:08 +02:00
print(f"PRICE: {close_price:.2f}")
2021-01-01 13:38:07 +01:00
# Update account balance etc. what is needed for trade
# -----
await update_account_balance()
if status == "SOLD" and signal_side == "BUY":
# -----
await new_limit_order(side=SIDE_BUY)
2021-01-10 21:39:06 +01:00
2025-02-16 12:47:07 +01:00
if model.get("no_trades_only_data_processing"):
2021-03-07 10:44:32 +01:00
print("SKIP TRADING due to 'no_trades_only_data_processing' parameter True")
# Never change status if orders not executed
2021-01-10 21:39:06 +01:00
else:
App.status = "BUYING"
2021-01-01 13:38:07 +01:00
elif status == "BOUGHT" and signal_side == "SELL":
# -----
await new_limit_order(side=SIDE_SELL)
2021-01-10 21:39:06 +01:00
2025-02-16 12:47:07 +01:00
if model.get("no_trades_only_data_processing"):
2021-03-07 10:44:32 +01:00
print("SKIP TRADING due to 'no_trades_only_data_processing' parameter True")
# Never change status if orders not executed
2021-01-10 21:39:06 +01:00
else:
App.status = "SELLING"
2021-01-01 13:38:07 +01:00
log.info(f"<=== End trade task.")
return
2021-10-23 21:51:31 +02:00
2021-01-01 13:38:07 +01:00
#
# Order and asset status
#
async def update_trade_status():
"""Read the account state and set the local state parameters."""
# GET /api/v3/openOrders - get current open orders
# GET /api/v3/allOrders - get all orders: active, canceled, or filled
2021-08-29 13:37:30 +02:00
symbol = App.config["symbol"]
2021-01-01 13:38:07 +01:00
# -----
try:
open_orders = App.client.get_open_orders(symbol=symbol) # By "open" orders they probably mean "NEW" or "PARTIALLY_FILLED"
# orders = App.client.get_all_orders(symbol=symbol, limit=10)
except Exception as e:
log.error(f"Binance exception in 'get_open_orders' {e}")
return
if not open_orders:
# -----
await update_account_balance()
2021-08-29 19:04:58 +02:00
last_kline = App.analyzer.get_last_kline(symbol)
2021-01-01 13:38:07 +01:00
last_close_price = to_decimal(last_kline[4]) # Close price of kline has index 4 in the list
base_quantity = App.base_quantity # BTC
2021-01-01 13:38:07 +01:00
btc_assets_in_usd = base_quantity * last_close_price # Cost of available BTC in USD
usd_assets = App.quote_quantity # USD
2021-01-01 13:38:07 +01:00
if usd_assets >= btc_assets_in_usd:
App.status = "SOLD"
2021-01-01 13:38:07 +01:00
else:
App.status = "BOUGHT"
2021-01-01 13:38:07 +01:00
elif len(open_orders) == 1:
order = open_orders[0]
if order.get("side") == SIDE_SELL:
App.status = "SELLING"
2021-01-01 13:38:07 +01:00
elif order.get("side") == SIDE_BUY:
App.status = "BUYING"
2021-01-01 13:38:07 +01:00
else:
log.error(f"Neither SELL nor BUY side of the order {order}.")
return None
else: # Many orders
log.error(f"Wrong state. More than one open order. Fix manually.")
return None
2021-10-23 21:51:31 +02:00
2021-01-01 13:38:07 +01:00
async def update_order_status():
"""
Update information about the current order and return its execution status.
ASSUMPTIONS and notes:
- Status codes: NEW PARTIALLY_FILLED FILLED CANCELED PENDING_CANCEL(currently unused) REJECTED EXPIRED
- only one or no orders can be active currently, but in future there can be many orders
- if no order id(s) is provided then retrieve all existing orders
"""
2021-08-29 13:37:30 +02:00
symbol = App.config["symbol"]
2021-01-01 13:38:07 +01:00
# Get currently active order and id (if any)
order = App.order
2021-01-01 13:38:07 +01:00
order_id = order.get("orderId", 0) if order else 0
if not order_id:
log.error(f"Wrong state or use: check order status cannot find the order id.")
return None
# -----
# Retrieve order from the server
try:
new_order = App.client.get_order(symbol=symbol, orderId=order_id)
except Exception as e:
log.error(f"Binance exception in 'get_order' {e}")
return
# Impose and overwrite the new order information
if new_order:
order.update(new_order)
else:
return None
# Now order["status"] contains the latest status of the order
return order["status"]
2021-10-23 21:51:31 +02:00
2021-01-01 13:38:07 +01:00
async def update_account_balance():
"""Get available assets (as decimal)."""
try:
2021-08-29 13:37:30 +02:00
balance = App.client.get_asset_balance(asset=App.config["base_asset"])
2021-01-01 13:38:07 +01:00
except Exception as e:
log.error(f"Binance exception in 'get_asset_balance' {e}")
return
App.base_quantity = Decimal(balance.get("free", "0.00000000")) # BTC
2021-01-01 13:38:07 +01:00
try:
2021-08-29 13:37:30 +02:00
balance = App.client.get_asset_balance(asset=App.config["quote_asset"])
2021-01-01 13:38:07 +01:00
except Exception as e:
log.error(f"Binance exception in 'get_asset_balance' {e}")
return
App.quote_quantity = Decimal(balance.get("free", "0.00000000")) # USD
2021-01-01 13:38:07 +01:00
pass
2021-10-23 21:51:31 +02:00
2021-01-01 13:38:07 +01:00
#
# Cancel and liquidation orders
#
async def cancel_order():
"""
Kill existing sell order. It is a blocking request, that is, it waits for the end of the operation.
Info: DELETE /api/v3/order - cancel order
"""
2021-08-29 13:37:30 +02:00
symbol = App.config["symbol"]
2021-01-01 13:38:07 +01:00
# Get currently active order and id (if any)
order = App.order
2021-01-01 13:38:07 +01:00
order_id = order.get("orderId", 0) if order else 0
if order_id == 0:
# TODO: Maybe retrieve all existing (sell, limit) orders
return None
# -----
try:
log.info(f"Cancelling order id {order_id}")
new_order = App.client.cancel_order(symbol=symbol, orderId=order_id)
except Exception as e:
log.error(f"Binance exception in 'cancel_order' {e}")
2021-03-07 10:44:32 +01:00
return None
2021-01-01 13:38:07 +01:00
# TODO: There is small probability that the order will be filled just before we want to kill it
# We need to somehow catch and process this case
# If we get an error (say, order does not exist and cannot be killed), then after error returned, we could do trade state reset
# Impose and overwrite the new order information
if new_order:
order.update(new_order)
else:
return None
# Now order["status"] contains the latest status of the order
return order["status"]
2021-10-23 21:51:31 +02:00
2021-01-01 13:38:07 +01:00
#
# Order creation
#
async def new_limit_order(side):
"""
Create a new limit sell order with the amount we current have.
The amount is total amount and price is determined according to our strategy (either fixed increase or increase depending on the signal).
"""
2021-08-29 13:37:30 +02:00
symbol = App.config["symbol"]
2021-01-01 13:38:07 +01:00
now_ts = now_timestamp()
2024-06-22 09:58:08 +02:00
trade_model = App.config.get("trade_model", {})
2021-01-01 13:38:07 +01:00
#
# Find limit price (from signal, last kline and adjustment parameters)
#
2021-08-29 19:04:58 +02:00
last_kline = App.analyzer.get_last_kline(symbol)
2021-01-01 13:38:07 +01:00
last_close_price = to_decimal(last_kline[4]) # Close price of kline has index 4 in the list
if not last_close_price:
log.error(f"Cannot determine last close price in order to create a market buy order.")
return None
2024-06-22 09:58:08 +02:00
price_adjustment = trade_model.get("limit_price_adjustment")
2021-01-01 13:38:07 +01:00
if side == SIDE_BUY:
price = last_close_price * Decimal(1.0 - price_adjustment) # Adjust price slightly lower
elif side == SIDE_SELL:
price = last_close_price * Decimal(1.0 + price_adjustment) # Adjust price slightly higher
price_str = round_str(price, 2)
price = Decimal(price_str) # We will use the adjusted price for computing quantity
#
# Find quantity
#
if side == SIDE_BUY:
# Find how much quantity we can buy for all available USD using the computed price
quantity = App.quote_quantity # USD
2024-06-22 09:58:08 +02:00
percentage_used_for_trade = trade_model.get("percentage_used_for_trade")
2021-01-01 13:38:07 +01:00
quantity = (quantity * percentage_used_for_trade) / Decimal(100.0) # Available for trade
quantity = quantity / price # BTC to buy
# Alternatively, we can pass quoteOrderQty in USDT (how much I want to spend)
elif side == SIDE_SELL:
# All available BTCs
quantity = App.base_quantity # BTC
2021-01-01 13:38:07 +01:00
quantity_str = round_down_str(quantity, 6)
#
# Execute order
#
order_spec = dict(
symbol=symbol,
side=side,
type=ORDER_TYPE_LIMIT, # Alternatively, ORDER_TYPE_LIMIT_MAKER
timeInForce=TIME_IN_FORCE_GTC,
quantity=quantity_str,
price=price_str,
)
2024-06-22 09:58:08 +02:00
if trade_model.get("no_trades_only_data_processing"):
2021-01-10 21:39:06 +01:00
print(f"NOT executed order spec: {order_spec}")
else:
order = execute_order(order_spec)
2021-01-01 13:38:07 +01:00
#
# Store/log order object in our records (only after confirmation of success)
#
App.order = order
App.order_time = now_ts
2021-01-01 13:38:07 +01:00
return order
2021-10-23 21:51:31 +02:00
2021-01-01 13:38:07 +01:00
def execute_order(order: dict):
"""Validate and submit order"""
2024-06-22 09:58:08 +02:00
trade_model = App.config.get("trade_model", {})
2021-01-01 13:38:07 +01:00
# TODO: Check validity, e.g., against filters (min, max) and our own limits
2024-06-22 09:58:08 +02:00
if trade_model.get("test_order_before_submit"):
2021-01-01 13:38:07 +01:00
try:
log.info(f"Submitting test order: {order}")
test_response = App.client.create_test_order(**order) # Returns {} if ok. Does not check available balances - only trade rules
except Exception as e:
log.error(f"Binance exception in 'create_test_order' {e}")
# TODO: Reset/resync whole account
return
2024-06-22 09:58:08 +02:00
if trade_model.get("simulate_order_execution"):
2021-01-01 13:38:07 +01:00
# TODO: Simply store order so that later we can check conditions of its execution
print(order)
pass
else:
# -----
# Submit order
try:
log.info(f"Submitting order: {order}")
order = App.client.create_order(**order)
except Exception as e:
log.error(f"Binance exception in 'create_order' {e}")
return
if not order or not order.get("status"):
return None
return order