CyberPunkMetalHead / gateio-crypto-trading-bot-binance-announcements-new-coins

This is a crypto trading bot that scans the Binance Annoucements page for new coins, and places trades on Gateio
MIT License
1.21k stars 303 forks source link

Handling of partially filled orders. "Not Enough balance" errors on sell. #54

Closed busyuqboy closed 2 years ago

busyuqboy commented 2 years ago

https://github.com/CyberPunkMetalHead/gateio-crypto-trading-bot-binance-announcements-new-coins/blob/master/main.py#L223

At line 223, we assume that the order has been filled. We write to the order.json file the full amount in this assumption. At this point, the order may be in a status of open, cancelled, filled or closed

One change I have made is: 1) to change the order create to use the time_in_force=ioc flag. Immediate or cancel. This makes my order automatically cancel if any or none is filled. The status is 'cancelled'. I don't ever want to leave an 'open' status order.

Here is my definition of my place_order method. Notice the ioc provision.

def place_order(base,quote, amount, side, last_price):
    """
    Args:
    'DOT', 'USDT', 50, 'buy', 400
    """
    try:
        order = Order(amount=str(float(amount)/float(last_price)), price=last_price, side=side, currency_pair=f'{base}_{quote}', time_in_force='ioc')
        order = spot_api.create_order(order)
        t = order
        logger.info(f"PLACE ORDER: {t.side} | {t.id} | {t.account} | {t.type} | {t.currency_pair} | {t.status} | amount={t.amount} | price={t.price} | left={t.left} | filled_total={t.filled_total} | fill_price={t.fill_price}")
    except Exception as e:
        logger.error(e)
        raise

    else:
        return order

The second and important change I made is to:

2) Update the buy amount in the order.json file to be accurate to what your order came back with.

At line 223, I have changed the code to this

else:
                        if test_mode:
                            order_status = order[announcement_coin]['status']
                        else:
                            order_status = order[announcement_coin]['_status']

                        message = f'Order created on {announcement_coin} at a price of {price} each.  {order_status=}'
                        logger.info(message)

                        if order_status == 'filled' or order_status == "closed":
                            if test_mode and float(order[announcement_coin]['_left']) > 0 and float(order[announcement_coin]['_amount']) > float(order[announcement_coin]['_left']):
                                # you can only sell what you have. Minus fees.  Look for unfulfilled
                                newAmount = float(order[announcement_coin]['_amount']) - float(order[announcement_coin]['_left']) - float(order[announcement_coin]['_fee'])
                                order[announcement_coin]['volume'] = newAmount
                            else:
                                store_order('order_fulfilled.json', order)

                                # you can only sell what you have. Minus fees.  Look for unfulfilled
                                newAmount = float(order[announcement_coin]['_amount']) - float(order[announcement_coin]['_left']) - float(order[announcement_coin]['_fee'])
                                order[announcement_coin]['_amount'] = newAmount

                            store_order('order.json', order)

                            if not test_mode and enable_sms:
                                try:
                                    send_sms_message(message)
                                except Exception:
                                    pass
                        elif order_status == 'open' or order_status == 'cancelled':
                            if not test_mode and order_status == 'open':
                                # cancel orders and try again in the next iteration
                                cancel_open_order(order[announcement_coin]['_id'], announcement_coin, pairing)
                                logger.info(f"Cancelled order {order[announcement_coin]['_id']} .  Waiting for status of 'filled/closed' for {announcement_coin}")

                            order.clear()  # reset for next iteration

You must deflate the order by the fee amount too to make sure you get a successful sell order filled.

busyuqboy commented 2 years ago

I've been learning that time_in_force="ioc" means that the order will be put into an automatic status of cancelled if it doesn't get filled (as opposed to open that will remain until closed). However, I've learned that in this scenario, you can also get partially filled cancelled orders.

I still believe, in terms of iterating and looking for prices, the ioc flag is the best way to go. But there still needs to be additions to handle a second buy order to try to achieve the remaing balance of volume specified in the config settings.

VuzzyM commented 2 years ago

I've been learning that time_in_force="ioc" means that the order will be put into an automatic status of cancelled if it doesn't get filled (as opposed to open that will remain until closed). However, I've learned that in this scenario, you can also get partially filled cancelled orders.

I still believe, in terms of iterating and looking for prices, the ioc flag is the best way to go. But there still needs to be additions to handle a second buy order to try to achieve the remaing balance of volume specified in the config settings.

@busyuqboy You can use list_spot_accounts with currency specified to check if the balance is enough. WalletApi.get_trade_fee(which is more recommended but requires wallet read-only access)

busyuqboy commented 2 years ago

PR will be coming soon for fixing: