Cinderella-Man / hands-on-elixir-and-otp-cryptocurrency-trading-bot-source-code

Resources related to the "Hands-on Elixir & OTP: Cryptocurrency trading bot" book
https://elixircryptobot.com
75 stars 24 forks source link

{:error, %Binance.InsufficientBalanceError{}} when Binance account has sufficient balance #20

Closed afs091 closed 1 year ago

afs091 commented 2 years ago

Hi Kamil,

Sometimes I received a {:error, %Binance.InsufficientBalanceError{reason: %{code: -2010, msg: "Account has insufficient balance for requested action."}}} message from Binance API while trying your solution for trading crypto in production environment.

I am using the source code of chapter 17 and I have sufficient balance in my Binance account.

This error occurs in Naive.Trader module when it calls:

Here are the logs:

            0:38:39.505 [info]  Trader(1633078420000) finished trade cycle for ADAEUR

            10:38:39.505 [info]  ADAEUR trader finished trade - restarting

            10:38:39.505 [info]  Initializing new trader(1633081119505) for ADAEUR

            10:38:42.780 [info]  The trader(1633081119505) is placing a BUY order for ADAEUR @ 1.86600000, quantity: 12.80000000

            10:49:07.846 [info]  Trader's(1633081119505 ADAEUR buy order got partially filled

            10:49:08.661 [info]  The trader(1633081119505) is placing a SELL order for ADAEUR @ 1.87100000, quantity: 12.80000000.

            10:49:09.479 [error] GenServer #PID<0.3395.0> terminating
            ** (MatchError) no match of right hand side value: {:error, %Binance.InsufficientBalanceError{reason: %{code: -2010, msg: "Account has insufficient balance for requested action."}}}
                (naive 0.1.0) lib/naive/trader.ex:142: Naive.Trader.handle_info/2
                (stdlib 3.15.2) gen_server.erl:695: :gen_server.try_dispatch/4
                (stdlib 3.15.2) gen_server.erl:771: :gen_server.handle_msg/6
                (stdlib 3.15.2) proc_lib.erl:226: :proc_lib.init_p_do_apply/3
            Last message: %Core.Struct.TradeEvent{buyer_market_maker: true, buyer_order_id: 216896572, event_time: 1633081748113, event_type: "trade", price: "1.86600000", quantity: "7.00000000", seller_order_id: 216897971, symbol: "ADAEUR", trade_id: 16058384, trade_time: 1633081748112}

            10:49:09.497 [error] GenServer :"Elixir.Naive.Leader-ADAEUR" terminating
            ** (Protocol.UndefinedError) protocol String.Chars not implemented for {{:badmatch, {:error, %Binance.InsufficientBalanceError{reason: %{code: -2010, msg: "Account has insufficient balance for requested action."}}}}, [{Naive.Trader, :handle_info, 2, [file: 'lib/naive/trader.ex', line: 142]}, {:gen_server, :try_dispatch, 4, [file: 'gen_server.erl', line: 695]}, {:gen_server, :handle_msg, 6, [file: 'gen_server.erl', line: 771]}, {:proc_lib, :init_p_do_apply, 3, [file: 'proc_lib.erl', line: 226]}]} of type Tuple. This protocol is implemented for the following type(s): Postgrex.Query, Postgrex.Copy, Decimal, BitString, Atom, Integer, NaiveDateTime, DateTime, Version.Requirement, Time, List, Float, Date, URI, Version
                (elixir 1.12.2) lib/string/chars.ex:3: String.Chars.impl_for!/1
                (elixir 1.12.2) lib/string/chars.ex:22: String.Chars.to_string/1
                (naive 0.1.0) lib/naive/leader.ex:180: Naive.Leader.handle_info/2
                (stdlib 3.15.2) gen_server.erl:695: :gen_server.try_dispatch/4
                (stdlib 3.15.2) gen_server.erl:771: :gen_server.handle_msg/6
                (stdlib 3.15.2) proc_lib.erl:226: :proc_lib.init_p_do_apply/3
            Last message: {:DOWN, #Reference<0.3730660033.40632321.49514>, :process, #PID<0.3395.0>, {{:badmatch, {:error, %Binance.InsufficientBalanceError{reason: %{code: -2010, msg: "Account has insufficient balance for requested action."}}}}, [{Naive.Trader, :handle_info, 2, [file: 'lib/naive/trader.ex', line: 142]}, {:gen_server, :try_dispatch, 4, [file: 'gen_server.erl', line: 695]}, {:gen_server, :handle_msg, 6, [file: 'gen_server.erl', line: 771]}, {:proc_lib, :init_p_do_apply, 3, [file: 'proc_lib.erl', line: 226]}]}}

            10:49:13.640 [info]  Initializing new trader(1633081753640) for ADAEUR

            10:49:18.591 [info]  The trader(1633081753640) is placing a BUY order for ADAEUR @ 1.86200000, quantity: 12.80000000

            11:25:11.583 [error] GenServer #PID<0.4469.0> terminating
            ** (MatchError) no match of right hand side value: {:error, %Binance.InsufficientBalanceError{reason: %{code: -2010, msg: "Account has insufficient balance for requested action."}}}
                (naive 0.1.0) lib/naive/trader.ex:79: Naive.Trader.handle_info/2
                (stdlib 3.15.2) gen_server.erl:695: :gen_server.try_dispatch/4
                (stdlib 3.15.2) gen_server.erl:771: :gen_server.handle_msg/6
                (stdlib 3.15.2) proc_lib.erl:226: :proc_lib.init_p_do_apply/3
            Last message: %Core.Struct.TradeEvent{buyer_market_maker: false, buyer_order_id: 216902085, event_time: 1633083910940, event_type: "trade", price: "1.86600000", quantity: "88.80000000", seller_order_id: 216901857, symbol: "ADAEUR", trade_id: 16059034, trade_time: 1633083910940}

            11:25:11.584 [error] GenServer :"Elixir.Naive.Leader-ADAEUR" terminating
            ** (Protocol.UndefinedError) protocol String.Chars not implemented for {{:badmatch, {:error, %Binance.InsufficientBalanceError{reason: %{code: -2010, msg: "Account has insufficient balance for requested action."}}}}, [{Naive.Trader, :handle_info, 2, [file: 'lib/naive/trader.ex', line: 79]}, {:gen_server, :try_dispatch, 4, [file: 'gen_server.erl', line: 695]}, {:gen_server, :handle_msg, 6, [file: 'gen_server.erl', line: 771]}, {:proc_lib, :init_p_do_apply, 3, [file: 'proc_lib.erl', line: 226]}]} of type Tuple. This protocol is implemented for the following type(s): Postgrex.Query, Postgrex.Copy, Decimal, BitString, Atom, Integer, NaiveDateTime, DateTime, Version.Requirement, Time, List, Float, Date, URI, Version
                (elixir 1.12.2) lib/string/chars.ex:3: String.Chars.impl_for!/1
                (elixir 1.12.2) lib/string/chars.ex:22: String.Chars.to_string/1
                (naive 0.1.0) lib/naive/leader.ex:180: Naive.Leader.handle_info/2
                (stdlib 3.15.2) gen_server.erl:695: :gen_server.try_dispatch/4
                (stdlib 3.15.2) gen_server.erl:771: :gen_server.handle_msg/6
                (stdlib 3.15.2) proc_lib.erl:226: :proc_lib.init_p_do_apply/3
            Last message: {:DOWN, #Reference<0.3730660033.40632322.112313>, :process, #PID<0.4469.0>, {{:badmatch, {:error, %Binance.InsufficientBalanceError{reason: %{code: -2010, msg: "Account has insufficient balance for requested action."}}}}, [{Naive.Trader, :handle_info, 2, [file: 'lib/naive/trader.ex', line: 79]}, {:gen_server, :try_dispatch, 4, [file: 'gen_server.erl', line: 695]}, {:gen_server, :handle_msg, 6, [file: 'gen_server.erl', line: 771]}, {:proc_lib, :init_p_do_apply, 3, [file: 'proc_lib.erl', line: 226]}]}}

As you can see, I am trading successfully and then suddenly I start getting errors due to insufficient balance but when I checked the Binance app I noticed that I have enough balance for the operation.

When the error occurs in {:ok, %Binance.OrderResponse{} = order} = @binance_client.order_limit_buy(symbol, quantity, price, "GTC") call the supervisor keeps restarting a new trader and eventually the trader receives a {:ok, %Binance.OrderResponse{} = order} message and continues.

In the other case, when the error occurs in {:ok, %Binance.OrderResponse{} = order} = @binance_client.order_limit_sell(symbol, quantity, sell_price, "GTC") the supervisor restarts a new trader which expects a trade event to place a new sell order but this trade event has already broadcasted when the error firstly occur. Consequently, the trader get stuck in that state and I have to manually sell the crypto in Binance app and to restart everything.

I think this error occurs due to some synchronization problem within the Binance data warehouses, so I modified the code to acknowledge this problem.

In Naive.Trader:

            def handle_info(
                    %TradeEvent{price: price},
                    %State{
                      id: id,
                      symbol: symbol,
                      budget: budget,
                      buy_order: nil,
                      buy_down_interval: buy_down_interval,
                      tick_size: tick_size,
                      step_size: step_size
                    } = state
                  ) do
                price = calculate_buy_price(price, buy_down_interval, tick_size)

                quantity = calculate_quantity(budget, price, step_size)

                @logger.info(
                  "The trader(#{id}) is placing a BUY order " <>
                    "for #{symbol} @ #{price}, quantity: #{quantity}"
                )

               new_state = case @binance_client.order_limit_buy(symbol, quantity, price, "GTC") do # => BEGIN UPDATE
                  {:ok, %Binance.OrderResponse{} = order} ->
                    :ok = broadcast_order(order)
                    new_state = %{state | buy_order: order}
                    @leader.notify(:trader_state_updated, new_state)
                    new_state

                  {:error, %Binance.InsufficientBalanceError{reason: reason}} ->
                    @logger.info(
                      "The trader(#{id}) recived an ERROR:  " <>
                        "#{reason}"
                    )
                    state
                end

                {:noreply, new_state}
              end

            ...

            def handle_info(
                    %TradeEvent{
                      buyer_order_id: order_id
                    } = trade_event,
                    %State{
                      id: id,
                      symbol: symbol,
                      buy_order:
                        %Binance.OrderResponse{
                          price: buy_price,
                          order_id: order_id,
                          orig_qty: quantity,
                          transact_time: timestamp
                        } = buy_order,
                      profit_interval: profit_interval,
                      tick_size: tick_size
                    } = state
                  ) do
                {:ok, %Binance.Order{} = current_buy_order} =
                  @binance_client.get_order(
                    symbol,
                    timestamp,
                    order_id
                  )

                :ok = broadcast_order(current_buy_order)

                buy_order = %{buy_order | status: current_buy_order.status}

                {operation_status, new_state} =
                  if buy_order.status == "FILLED" do
                    sell_price = calculate_sell_price(buy_price, profit_interval, tick_size)

                    @logger.info(
                      "The trader(#{id}) is placing a SELL order for " <>
                        "#{symbol} @ #{sell_price}, quantity: #{quantity}."
                    )

                    case @binance_client.order_limit_sell(symbol, quantity, sell_price, "GTC") do # => BEGIN UPDATE
                      {:ok, %Binance.OrderResponse{} = order} ->
                        :ok = broadcast_order(order)
                        {:ok, %{state | buy_order: buy_order, sell_order: order}}

                      {:error, %Binance.InsufficientBalanceError{reason: reason}} ->
                        @logger.info(
                          "The trader(#{id}) recived an ERROR:  " <>
                            "#{reason}"
                        )

                        {:error, state}
                    end
                  else
                    @logger.info("Trader's(#{id}) #{symbol} buy order got partially filled")
                    {:ok, %{state | buy_order: buy_order}}
                  end

                case operation_status do
                  :ok -> @leader.notify(:trader_state_updated, new_state)
                  :error -> @leader.notify(:rebroadcast_trade_event, trade_event)
                end

                {:noreply, new_state}
              end

And in the Naive.Leader:

            def notify(:rebroadcast_trade_event, trade_event) do
                Phoenix.PubSub.broadcast(
                  Core.PubSub,
                  "TRADE_EVENTS:#{trade_event.symbol}",
                  trade_event
                )
              end

So, let me know your thoughts about this kind of problem and if you have another solution to solve it.

Thank you very much for your attention!

André Santos

Cinderella-Man commented 2 years ago

Hi André,

Thank you very much for raising this issue. I spent quite a bit of time today, and I think I know what's wrong. First of all, you are right - this error should be handled more gracefully.

Why does it happen?

My current theory is that the quantity when selling is broken. When you buy/sell, you pay the fee in the currency you own before the exchange. So when you are buying 12.80000000 ADAs for EURos - you will pay the fee in EURos. Then when you want to sell it, you can't sell 12.80000000 ADA as you need to leave enough for a selling fee (which will be in ADA).

I never realised that this is a problem as I'm paying my fees using BNB (and honestly forgot that it's still set up to do that) :facepalm:

I bet that subtracting the fee from the quantity of ADA on that sell order would fix it.

That would explain the selling. Buying error is a result of trying to buy when you already purchased, but the process died on selling :shrug: (I'm not sure why the state wasn't picked up from the leader :thinking: - completely different problem)

Let me know what do you think about my idea - it shouldn't be complicated to fix this(calculating the correct sell quantity) and give it a try as you have it all set up :wink:

afs091 commented 2 years ago

Hi Kamil,

Thank you for helping me with this rookie mistake!

I think that you correctly spot the problem. I don't have currency to pay the fees :facepalm: :facepalm: :facepalm:

I will setup some currency on both trade sides to pay the fees and try it first. But I am afraid that eventually I will run out these currencies that I set aside for paying fees and the error will raise again. If that happens I will try to calculate the sell quantity by subtracting the the fee from the quantity of ADA.

Thank you very much!