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-03-15 20:31:12 +01:00 | | |
|
2020-02-23 20:45:50 +01:00 | | | import pandas as pd
|
| | | import asyncio
|
| | |
|
| | | from binance.client import Client
|
| | | 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 *
|
2021-09-04 18:19:04 +02:00 | | | from service.analyzer import *
|
2020-02-23 20:45:50 +01:00 | | |
|
| | | import logging
|
2021-09-04 21:28:45 +02:00 | | | log = logging.getLogger('trader')
|
2021-01-01 13:38:07 +01:00 | | | logging.basicConfig(
|
| | | filename="trader.log", # parameter in App
|
| | | level=logging.DEBUG,
|
| | | #format = "%(asctime)s.%(msecs)03d %(levelname)s %(module)s - %(funcName)s: %(message)s",
|
| | | format = "%(asctime)s %(levelname)s %(message)s",
|
| | | #datefmt = '%Y-%m-%d %H:%M:%S',
|
| | | )
|
2020-12-13 15:06:24 +01:00 | | |
|
2021-01-01 13:38:07 +01:00 | | |
|
| | | async def main_trader_task():
|
| | | """
|
| | | 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.
|
| | | """
|
2021-08-29 13:37:30 +02:00 | | | symbol = App.config["symbol"]
|
2021-01-01 13:38:07 +01:00 | | | startTime, endTime = get_interval("1m")
|
| | | now_ts = now_timestamp()
|
2020-03-25 20:38:40 +01:00 | | |
|
2021-01-01 13:38:07 +01:00 | | | log.info(f"===> Start trade task. Timestamp {now_ts}. Interval [{startTime},{endTime}].")
|
2020-03-30 20:54:26 +02:00 | | |
|
2021-01-01 13:38:07 +01:00 | | | #
|
| | | # Sync trade status, check running orders (orders, account etc.)
|
| | | #
|
2020-03-26 15:13:51 +01:00 | | |
|
2021-08-29 13:17:22 +02:00 | | | status = App.status
|
2020-03-25 20:38:40 +01:00 | | |
|
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()
|
| | |
|
2021-08-29 13:17:22 +02:00 | | | 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}")
|
2021-08-29 13:17:22 +02:00 | | | 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}")
|
2021-08-29 13:17:22 +02:00 | | | 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":
|
2021-08-29 13:17:22 +02:00 | | | App.status = "SOLD"
|
2021-01-01 13:38:07 +01:00 | | | elif status == "SELLING":
|
2021-08-29 13:17:22 +02:00 | | | 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)
|
| | | #
|
| | |
|
2021-08-29 13:17:22 +02:00 | | | status = App.status
|
| | | signal = App.signal
|
2021-01-01 13:38:07 +01:00 | | | signal_side = signal.get("side")
|
| | |
|
| | | # 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":
|
2021-08-29 13:17:22 +02:00 | | | App.status = "SOLD"
|
2021-01-01 13:38:07 +01:00 | | | elif status == "SELLING":
|
2021-08-29 13:17:22 +02:00 | | | App.status = "BOUGHT"
|
2021-01-01 13:38:07 +01:00 | | |
|
| | | #
|
| | | # Trade by creating orders
|
| | | #
|
| | |
|
2021-08-29 13:17:22 +02:00 | | | status = App.status
|
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:
|
2021-03-07 10:44:32 +01:00 | | | print(f"SCORE: {signal.get('score'):+.3f}. PRICE: {signal.get('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 | | |
|
2021-08-29 13:37:30 +02:00 | | | if App.config["trader"]["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:
|
2021-08-29 13:17:22 +02:00 | | | 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 | | |
|
2021-08-29 13:37:30 +02:00 | | | if App.config["trader"]["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:
|
2021-08-29 13:17:22 +02:00 | | | 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
|
| | |
|
2021-08-29 13:17:22 +02:00 | | | 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
|
| | |
|
2021-08-29 13:17:22 +02:00 | | | usd_assets = App.quote_quantity # USD
|
2021-01-01 13:38:07 +01:00 | | |
|
| | | if usd_assets >= btc_assets_in_usd:
|
2021-08-29 13:17:22 +02:00 | | | App.status = "SOLD"
|
2021-01-01 13:38:07 +01:00 | | | else:
|
2021-08-29 13:17:22 +02:00 | | | 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:
|
2021-08-29 13:17:22 +02:00 | | | App.status = "SELLING"
|
2021-01-01 13:38:07 +01:00 | | | elif order.get("side") == SIDE_BUY:
|
2021-08-29 13:17:22 +02:00 | | | 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)
|
2021-08-29 13:17:22 +02:00 | | | 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
|
| | |
|
2021-08-29 13:17:22 +02:00 | | | 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
|
| | |
|
2021-08-29 13:17:22 +02:00 | | | 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)
|
2021-08-29 13:17:22 +02:00 | | | 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()
|
| | |
|
| | | #
|
| | | # 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
|
| | |
|
2021-08-29 13:37:30 +02:00 | | | price_adjustment = App.config["trader"]["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
|
2021-08-29 13:17:22 +02:00 | | | quantity = App.quote_quantity # USD
|
2021-08-29 13:37:30 +02:00 | | | percentage_used_for_trade = App.config["trader"]["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
|
2021-08-29 13:17:22 +02:00 | | | 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,
|
| | | )
|
| | |
|
2021-08-29 13:37:30 +02:00 | | | if App.config["trader"]["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)
|
| | | #
|
2021-08-29 13:17:22 +02:00 | | | 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"""
|
| | |
|
| | | # TODO: Check validity, e.g., against filters (min, max) and our own limits
|
| | |
|
2021-08-29 13:37:30 +02:00 | | | if App.config["trader"]["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
|
| | |
|
2021-08-29 13:37:30 +02:00 | | | if App.config["trader"]["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)
|
2021-08-29 13:17:22 +02:00 | | | print(App.signal)
|
2021-01-01 13:38:07 +01:00 | | | 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
|