Chapter 7 Introduce a trader budget and calculating the quantity

7.1 Objectives

  • fetch step_size
  • append budget and step_size to the Trader’s state compiled by the Leader
  • append budget and step_size to the Trader’s state
  • calculate quantity

7.2 Fetch step_size

In the 2nd chapter we hardcoded quantity to 100, it’s time to refactor that. We will need step_size information from the Binance which we are already retrieving together with tick_size in the exchangeInfo call(but not getting it out from the response). So we will rename the fetch_tick_size/1 function to fetch_symbol_filters/1 which will allow us to return multiple filters(tick_size and step_size) from that function.

  # /apps/naive/lib/naive/leader.ex
  ...
  defp fetch_symbol_settings(symbol) do
    symbol_filters = fetch_symbol_filters(symbol) # <= updated fetch_tick_size
    
    Map.merge(
      %{
        symbol: symbol,
        chunks: 1,
        budget: 20,
        # -0.01% for quick testing
        buy_down_interval: "0.0001",
        # -0.12% for quick testing
        profit_interval: "-0.0012"
      },
      symbol_filters
    )
  end

  defp fetch_symbol_filters(symbol) do  # <= updated fetch_tick_size
    symbol_filters =
      @binance_client.get_exchange_info()
      |> elem(1)
      |> Map.get(:symbols)
      |> Enum.find(&(&1["symbol"] == symbol))
      |> Map.get("filters")

    tick_size =
      symbol_filters
      |> Enum.find(&(&1["filterType"] == "PRICE_FILTER"))
      |> Map.get("tickSize")

    step_size =
      symbol_filters
      |> Enum.find(&(&1["filterType"] == "LOT_SIZE"))
      |> Map.get("stepSize")

    %{
      tick_size: tick_size,
      step_size: step_size
    }
  end

Instead of reassigning the filters one by one into the settings, we will merge them together(#1). Additionally, we will introduce a budget(#2) which will be shared across all traders of the symbol. Also, we don’t need to assign tick_size here as it’s part of the settings that are merged.

7.3 Append budget and step_size to the Trader’s state inside the Leader

The budget needs to be added to the %State{}(step_size will be automatically passed on by struct/2) of the trader inside fresh_trader_state/1(where we initialize the state of traders). Before we will assign it we need to divide it by the number of chunks as each trader gets only a chunk of the budget:

  # /apps/naive/lib/naive/leader.ex
  defp fresh_trader_state(settings) do
    %{
      struct(Trader.State, settings)
      | budget: D.div(settings.budget, settings.chunks)
    }
  end

In the code above we are using the Decimal module(aliased as D) to calculate the budget - we need to alias it at the top of Naive.Leader’s file:

# /apps/naive/lib/naive/leader.ex
defmodule Naive.Leader do
  use GenServer

  alias Decimal, as: D # <= add this line
  alias Naive.Trader
  ...

7.4 Append budget and step_size to the Trader’s state

We need to add both budget and step_size to the Naive.Trader’s state struct:

  # /apps/naive/lib/naive/trader.ex
  ...
  defmodule State do
    @enforce_keys [
      :symbol,
      :budget, # <= add this line
      :buy_down_interval,
      :profit_interval,
      :tick_size,
      :step_size # <= add this line and comma above
    ]
    defstruct [
      :symbol,
      :budget, # <= add this line
      :buy_order,
      :sell_order,
      :buy_down_interval,
      :profit_interval,
      :tick_size,
      :step_size # <= add this line and comma above
    ]
  end
  ...

7.5 Calculate quantity

Jumping back to the handle_info/2 where the Naive.Trader places a buy order, we need to pattern match on the step_size and budget then we will be able to swap hardcoded quantity with the result of calling the calculate_quantity/3 function:

  # /apps/naive/lib/naive/trader.ex
  ...
  def handle_info(
        %TradeEvent{price: price},
        %State{
          symbol: symbol,
          budget: budget, # <= add this line
          buy_order: nil,
          buy_down_interval: buy_down_interval,
          tick_size: tick_size,
          step_size: step_size # <= add this line
        } = state
      ) do
    ...
    quantity = calculate_quantity(budget, price, step_size)
    ...

To calculate quantity we will just divide the budget by the price with a caveat that it’s possible (as with calculating the price) that it’s not a legal quantity value as it needs to be divisible by step_size:

  # /apps/naive/lib/naive/trader.ex
  # add below at the bottom of the file
  ...
  defp calculate_quantity(budget, price, step_size) do
    # not necessarily legal quantity
    exact_target_quantity = D.div(budget, price)

    D.to_string(
      D.mult(
        D.div_int(exact_target_quantity, step_size),
        step_size
      ),
      :normal
    )
  end

7.5.1 IEx testing

That finishes the quantity(and budget) implementation, we will jump into the IEx session to see how it works.

First, start the streaming and trading on the same symbol and a moment later you should see a variable amount of quantity that more or less uses the full allowed budget:

$ iex -S mix
...
iex(1)> Streamer.start_streaming("XRPUSDT")
{:ok, #PID<0.313.0>}
iex(2)> Naive.start_trading("XRPUSDT")
21:16:14.829 [info]  Starting new supervision tree to trade on XRPUSDT
21:16:16.755 [info]  Initializing new trader for XRPUSDT
21:16:20.009 [info]  Placing BUY order for XRPUSDT @ 0.29506, quantity: 67.7
21:16:23.456 [info]  Buy order filled, placing SELL order for XRPUSDT @ 0.29529,
quantity: 67.7

As we can see our Naive.Trader process is now buying and selling based on passed budget.

[Note] Please remember to run the mix format to keep things nice and tidy.

The source code for this chapter can be found on GitHub