benonymus

benonymus

Two lists merging/unifying

Hey, I have a list of inputs, that can be a buy or a sell.
I sort these according to being a sell or a buy and merge the ones with the same price(to be one unit with the sum of the amounts). Also I can keep them in one list with the same result.

What I want to do that with in mind of the order, I want the buys to hit out the sells, if the price is equal or lower, a transaction to happen sort of.

I can’t really wrap my head around it so far,
as I imagine I would need to go on item basis, like take the first from buy and sell, compare them → make a decision that can be transaction and update the values/remove them → or keep both of the items. But then on the next round I would need to keep the result of this in mind, and check on it first.

Any thoughts?

Sorry, for not giving context, so,
yes the other question is related, but this introduces a new problem, but that one was solved.

The problem:

I have this json:

{
   "orders": [
      {"command": "sell", "price": 100.003, "amount": 2.4},
      {"command": "buy", "price": 90.394, "amount": 3.445},
      {"command": "buy", "price": 89.394, "amount": 4.3},
      {"command": "sell", "price": 100.013, "amount": 2.2},
      {"command": "buy", "price": 90.15, "amount": 1.305},
      {"command": "buy", "price": 90.394, "amount": 1.0},
      {"command": "sell", "price": 90.394, "amount": 2.2}   
   ]
}

and the expected output is:

{
   "buy": [
     { 
       "price": 90.394,
       "volume": 2.245
     },
     { 
       "price": 90.15,
       "volume": 1.305
     },
     { 
       "price": 89.394,
       "volume": 4.3
     },
   ],
   "sell": [
     { 
       "price": 100.003,
       "volume": 2.4
     },
     { 
       "price": 100.013,
       "volume": 2.2
     }
   ]
}

all the sorting aside, as you see 7 goes in and 5 comes out.
the ones with the same price are added up or if there is a sell and a buy with the same price those take care of each other too.
I solved this by doing this after creating the orders:
my code here works on one list, and then i split that into the buy and sell lists for displaying, but if i split the 2 lists and then pass that list to this function we in the same spot

defp transactions(limit_orders) do
    for order <- limit_orders do
      Enum.reduce(
        limit_orders,
        order,
        fn x, y ->
          cond do
            Decimal.cmp(x.price, y.price) == :eq and x.id != y.id and x.command == y.command ->
              new_amount = Decimal.add(x.amount, y.amount)

              y
              |> Map.put(:amount, new_amount)
              |> Map.put(:id, x.id)

            Decimal.cmp(x.price, y.price) == :eq and x.command != y.command ->
              case Decimal.cmp(x.amount, y.amount) do
                :gt ->
                  new_amount = Decimal.sub(x.amount, y.amount)
                  Map.put(x, :amount, new_amount)

                :lt ->
                  new_amount = Decimal.sub(y.amount, x.amount)
                  Map.put(y, :amount, new_amount)

                :eq ->
                  nil
              end

            true ->
              y
          end
        end
      )
    end
    |> Enum.filter(&(!is_nil(&1)))
    |> Enum.uniq_by(fn x -> x.id end)
  end

The new problem:
let’s say I extend the previous order list with a few new orders:

{
   "orders": [
      {"command": "sell", "price": 100.003, "amount": 2.4},
      {"command": "buy", "price": 90.394, "amount": 3.445},
      {"command": "buy", "price": 89.394, "amount": 4.3},
      {"command": "sell", "price": 100.013, "amount": 2.2},
      {"command": "buy", "price": 90.15, "amount": 1.305},
      {"command": "buy", "price": 90.394, "amount": 1.0},
      {"command": "sell", "price": 90.394, "amount": 2.2},
new one below:
      {"command": "sell", "price": 90.15, "amount": 3.4}     
   ]
}

As you can see from the output of the previous one the the lowest buy price is higher than the lowest sell price 90.15 here. So that transaction should happen as well.
After that, we compare volume of 90.394 with amount of the sell command. volume of 90.394 is 2.245 and amount of 90.15 is 3.4. So that, we’ve amount left is 3.4 - 2.245 = 1.155 There is amount left. Then, this is partially matched! This command has to continue matching next buy price.
Continue next price, We compare 90.15 with 90.15 on Buy side. It is matched again. because the price on both side is equal. Let’s check the volume on buy side with the amount left which is 1.155 - 1.305 = -0.15. It means there is no amount left. And now volume on buy side would be 0.15.

So the expect output after this one is this:

{
   "buy": [
     { 
       "price": 90.15,
       "volume": 0.15
     },
     { 
       "price": 89.394,
       "volume": 4.3
     },
   ],
   "sell": [
     { 
       "price": 100.003,
       "volume": 2.4
     },
     { 
       "price": 100.013,
       "volume": 2.2
     }
   ]
}

my code in it’s current state puts out this:

{
    "buy": [
        {
            "price": "90.394",
            "volume": "2.245"
        },
        {
            "price": "89.394",
            "volume": "4.3"
        }
    ],
    "sell": [
        {
            "price": "90.15",
            "volume": "2.095"
        },
        {
            "price": "100.003",
            "volume": "2.4"
        },
        {
            "price": "100.013",
            "volume": "2.2"
        }
    ]
}

since it doesn’t match on higher buying price, only if it matches exactly.
I made another case where it would match if there is a higher buying price in a similar fashion to the matching price, but if i add other orders as well:

{
   "orders": [
      {"command": "sell", "price": 100.003, "amount": 2.4},
      {"command": "buy", "price": 90.394, "amount": 3.445},
      {"command": "buy", "price": 89.394, "amount": 4.3},
      {"command": "sell", "price": 100.013, "amount": 2.2},
      {"command": "buy", "price": 90.15, "amount": 1.305},
      {"command": "buy", "price": 90.394, "amount": 1.0},
      {"command": "sell", "price": 90.394, "amount": 2.2},
      {"command": "sell", "price": 90.15, "amount": 3.4},      
      {"command": "buy", "price": 91.33, "amount": 1.8},      
      {"command": "buy", "price": 100.01, "amount": 4.0},        
      {"command": "sell", "price": 100.15, "amount": 3.8}          
   ]
}

th3 3 bottom ones,
since my “design” doesn’t respect the order of the commands strictly put, it wont give the desired output:

{
   "buy": [
     { 
       "price": 100.01,
       "volume": 1.6
     },
     { 
       "price": 91.33,
       "volume": 1.8
     },
     { 
       "price": 90.15,
       "volume": 0.15
     },
     { 
       "price": 89.394,
       "volume": 4.3
     },
   ],
   "sell": [
     { 
       "price": 100.013,
       "volume": 2.2
     },
     { 
       "price": 100.15,
       "volume": 3.8
     }
   ]
}

because it would match on the exactly matching prices first

First Post!

ericgray

ericgray

It would be helpful if you could post some code of what you tried so far.

Most Liked

benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

This seems like this is part of this thread: Merging maps in list based on matching value - #6 by dimitarvp ?

benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

Let’s step back from the details of the grouping / buy / sell algorithm for a second and look at this more generally. @benonymus you have a clear idea in your head about what the rules are. However you were unclear about how to work with the two lists. Hopefully these responses have provided some insight into how to work with multiple lists.

What I’d recommend at this point is instead of trying to communicate the exact details of the algorithm to people here so that they can write out an exact solution, instead focus on any remaining questions you have about the Elixir mechanics of dealing with these lists, and then take a crack at it on your own. We’ll be here to review code or questions you may have.

peerreynders

peerreynders

I don’t recall that your initial statement mentioned that order was significant.

So just brute force it:

Session:

$ elixir demo.exs
data
{[
   %{amount: 2245, command: :buy, price: 90394},
   %{amount: 4300, command: :buy, price: 89394},
   %{amount: 1305, command: :buy, price: 90150}
 ],
 [
   %{amount: 2400, command: :sell, price: 100003},
   %{amount: 2200, command: :sell, price: 100013}
 ]}
data2
{[
   %{amount: 4300, command: :buy, price: 89394},
   %{amount: 150, command: :buy, price: 90150}
 ],
 [
   %{amount: 2400, command: :sell, price: 100003},
   %{amount: 2200, command: :sell, price: 100013}
 ]}
data3
{[
   %{amount: 4300, command: :buy, price: 89394},
   %{amount: 150, command: :buy, price: 90150},
   %{amount: 1800, command: :buy, price: 91330},
   %{amount: 1600, command: :buy, price: 100010}
 ],
 [
   %{amount: 2200, command: :sell, price: 100013},
   %{amount: 3800, command: :sell, price: 100150}
 ]}

Code:

defmodule Demo do
  def run(data),
    do: List.foldl(data, {[], []}, &reducer/2)

  defp reducer(%{command: :sell} = sell, {buys, sells}) do
    case apply_sell(sell, buys, []) do
      {nil, new_buys} ->
        {new_buys, sells}

      {new_sell, new_buys} ->
        {new_buys, combine(sells, new_sell, [])}
    end
  end

  defp reducer(%{command: :buy} = buy, {buys, sells}) do
    case apply_buy(buy, sells, []) do
      {nil, new_sells} ->
        {buys, new_sells}

      {new_buy, new_sells} ->
        {combine(buys, new_buy, []), new_sells}
    end
  end

  defp apply_buy(buy, [], other) do
    {buy, :lists.reverse(other)}
  end

  defp apply_buy(
         %{price: buy_price, amount: buy_amount} = buy,
         [%{price: sell_price, amount: sell_amount} = sell | tail],
         other
       )
       when buy_price >= sell_price do
    cond do
      buy_amount > sell_amount ->
        apply_buy(%{buy | amount: buy_amount - sell_amount}, tail, other)

      buy_amount < sell_amount ->
        {nil, :lists.reverse(other, [%{sell | amount: sell_amount - buy_amount} | tail])}

      true ->
        {nil, :lists.reverse(other, tail)}
    end
  end

  defp apply_buy(buy, [sell | tail], other) do
    # buy price too low
    apply_buy(buy, tail, [sell | other])
  end

  defp apply_sell(sell, [], other) do
    {sell, :lists.reverse(other)}
  end

  defp apply_sell(
         %{price: sell_price, amount: sell_amount} = sell,
         [%{price: buy_price, amount: buy_amount} = buy | tail],
         other
       )
       when sell_price <= buy_price do
    cond do
      sell_amount > buy_amount ->
        apply_sell(%{sell | amount: sell_amount - buy_amount}, tail, other)

      sell_amount < buy_amount ->
        {nil, :lists.reverse(other, [%{buy | amount: buy_amount - sell_amount} | tail])}

      true ->
        {nil, :lists.reverse(other, tail)}
    end
  end

  defp apply_sell(sell, [buy | tail], other) do
    # sell price too high
    apply_sell(sell, tail, [buy | other])
  end

  defp combine([], item, other),
    do: :lists.reverse(other, [item])

  defp combine(
         [%{price: head_price, amount: head_amount} = head | tail],
         %{price: item_price, amount: item_amount},
         other
       )
       when head_price === item_price,
       do: :lists.reverse(other, [%{head | amount: head_amount + item_amount} | tail])

  defp combine([head | tail], item, other),
    do: combine(tail, item, [head | other])
end

data = [
  %{command: :sell, price: 100_003, amount: 2_400},
  %{command: :buy, price: 90_394, amount: 3_445},
  %{command: :buy, price: 89_394, amount: 4_300},
  %{command: :sell, price: 100_013, amount: 2_200},
  %{command: :buy, price: 90_150, amount: 1_305},
  %{command: :buy, price: 90_394, amount: 1_000},
  %{command: :sell, price: 90_394, amount: 2_200}
]

IO.puts("data")
IO.inspect(Demo.run(data))

data2 =
  data ++
    [%{command: :sell, price: 90_150, amount: 3_400}]

IO.puts("data2")
IO.inspect(Demo.run(data2))

data3 =
  data2 ++
    [
      %{command: :buy, price: 91_330, amount: 1_800},
      %{command: :buy, price: 100_010, amount: 4_000},
      %{command: :sell, price: 100_150, amount: 3_800}
    ]

IO.puts("data3")
IO.inspect(Demo.run(data3))

Last Post!

benonymus

benonymus

After some research I figured out that what I need is a matching/trading engine:
https://medium.com/lgogroup/a-matching-engine-for-our-values-part-1-795a29b400fa

Does anyone have an example?

Where Next?

Popular in Questions Top

Qqwy
Original source of discussion: This topic on the Pragmatic Programmers’ Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? Ecto.Repo — Ecto v3.14.0 has exampl...
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New

Other popular topics Top

jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New
dogweather
I wrote this comment on r/haskell, and it’s not popular there. :wink: But I think I’m on to something… Haskell reminds me of Java, and e...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New

We're in Beta

About us Mission Statement