diff --git a/05-enable-parallel-trading.Rmd b/05-enable-parallel-trading.Rmd index 357a7c2..029454a 100644 --- a/05-enable-parallel-trading.Rmd +++ b/05-enable-parallel-trading.Rmd @@ -426,7 +426,7 @@ trader locally: ) do case Enum.find_index(traders, &(&1.pid == trader_pid)) do nil -> - Logger.warn( + Logger.warning( "Tried to update the state of trader that leader is not aware of" ) {:reply, :ok, state} @@ -458,7 +458,7 @@ First, trade finished scenario. As previously, we will try to find the trader da case Enum.find_index(traders, &(&1.pid == trader_pid)) do nil -> - Logger.warn( + Logger.warning( "Tried to restart finished #{symbol} " <> "trader that leader is not aware of" ) @@ -489,7 +489,7 @@ The final callback that we need to provide will handle the scenario where the tr case Enum.find_index(traders, &(&1.pid == trader_pid)) do nil -> - Logger.warn( + Logger.warning( "Tried to restart #{symbol} trader " <> "but failed to find its cached state" ) diff --git a/09-run-multiple-parallel-traders.Rmd b/09-run-multiple-parallel-traders.Rmd index ad4a69f..85e3194 100644 --- a/09-run-multiple-parallel-traders.Rmd +++ b/09-run-multiple-parallel-traders.Rmd @@ -186,7 +186,7 @@ Moving on to the body of our callback. As with other ones, we will check can we # body of our callback case Enum.find_index(traders, &(&1.pid == trader_pid)) do nil -> - Logger.warn("Rebuy triggered by trader that leader is not aware of") + Logger.warning("Rebuy triggered by trader that leader is not aware of") {:reply, :ok, state} index -> diff --git a/11-supervise-and-autostart-streaming.Rmd b/11-supervise-and-autostart-streaming.Rmd index b2769fc..f1e3e0a 100644 --- a/11-supervise-and-autostart-streaming.Rmd +++ b/11-supervise-and-autostart-streaming.Rmd @@ -285,7 +285,7 @@ Next, we will add the `start_streaming/1` function at the bottom of the {:ok, _pid} = start_streamer(symbol) pid -> - Logger.warn("Streaming on #{symbol} already started") + Logger.warning("Streaming on #{symbol} already started") {:ok, _settings} = update_streaming_status(symbol, "on") {:ok, pid} end @@ -378,7 +378,7 @@ Let's write a `stop_streaming/1` logic inside the `Streamer.DynamicStreamerSuper def stop_streaming(symbol) when is_binary(symbol) do case get_pid(symbol) do nil -> - Logger.warn("Streaming on #{symbol} already stopped") + Logger.warning("Streaming on #{symbol} already stopped") {:ok, _settings} = update_streaming_status(symbol, "off") pid -> diff --git a/12-start-stop-and-autostart-trading.Rmd b/12-start-stop-and-autostart-trading.Rmd index 7652040..ec52d83 100644 --- a/12-start-stop-and-autostart-trading.Rmd +++ b/12-start-stop-and-autostart-trading.Rmd @@ -69,7 +69,7 @@ Our `start_trading/1` implementation is almost the same as one for the `streamer {:ok, _pid} = start_symbol_supervisor(symbol) pid -> - Logger.warn("Trading on #{symbol} already started") + Logger.warning("Trading on #{symbol} already started") {:ok, _settings} = update_trading_status(symbol, "on") {:ok, pid} end @@ -142,7 +142,7 @@ Stop trading will require a change in two places, first inside the `Naive.Dynami case get_pid(symbol) do nil -> - Logger.warn("Trading on #{symbol} already stopped") + Logger.warning("Trading on #{symbol} already stopped") {:ok, _settings} = update_trading_status(symbol, "off") pid -> @@ -309,7 +309,7 @@ Next, we will create a `shutdown_trading/1` function inside the `Naive.DynamicSy case get_pid(symbol) do nil -> - Logger.warn("Trading on #{symbol} already stopped") + Logger.warning("Trading on #{symbol} already stopped") {:ok, _settings} = update_trading_status(symbol, "off") _pid -> @@ -367,7 +367,7 @@ Let's look at the updated implementation of the "end of trade" handler: case Enum.find_index(traders, &(&1.pid == trader_pid)) do nil -> - Logger.warn( + Logger.warning( "Tried to restart finished #{symbol} " <> "trader that leader is not aware of" ) @@ -381,7 +381,7 @@ Let's look at the updated implementation of the "end of trade" handler: index -> new_traders = if settings.status == "shutdown" do # <= refactored code - Logger.warn( + Logger.warning( "The leader won't start a new trader on #{symbol} " <> "as symbol is in the 'shutdown' state" ) @@ -416,7 +416,7 @@ The second callback that we need to modify is the `rebuy` triggered: ) do case Enum.find_index(traders, &(&1.pid == trader_pid)) do nil -> - Logger.warn("Rebuy triggered by trader that leader is not aware of") + Logger.warning("Rebuy triggered by trader that leader is not aware of") {:reply, :ok, state} index -> @@ -430,7 +430,7 @@ The second callback that we need to modify is the `rebuy` triggered: updated_traders else if settings.status == "shutdown" do - Logger.warn( + Logger.warning( "The leader won't start a new trader on #{symbol} " <> "as symbol is in the 'shutdown' state" ) diff --git a/13-abstract-duplicated-code-using-macros.Rmd b/13-abstract-duplicated-code-using-macros.Rmd index 41bd8ab..e548c10 100644 --- a/13-abstract-duplicated-code-using-macros.Rmd +++ b/13-abstract-duplicated-code-using-macros.Rmd @@ -120,7 +120,7 @@ The `fetch_symbols_to_trade/0` will get updated to `fetch_symbols_to_start/0`: ) # ^^^^^^ inlined `start_symbol_supervisor/1` pid -> - Logger.warn("Trading on #{symbol} already started") + Logger.warning("Trading on #{symbol} already started") {:ok, _settings} = update_status(symbol, "on") # <= updated name {:ok, pid} end @@ -150,7 +150,7 @@ Last function to rename in this module will be the `stop_trading/1` to `stop_wor def stop_worker(symbol) when is_binary(symbol) do # <= updated name case get_pid(symbol) do nil -> - Logger.warn("Trading on #{symbol} already stopped") + Logger.warning("Trading on #{symbol} already stopped") {:ok, _settings} = update_status(symbol, "off") # <= updated name pid -> @@ -213,7 +213,7 @@ We also need to update the `shutdown_trading/1` function as we removed all the p def shutdown_worker(symbol) when is_binary(symbol) do # <= updated name case Core.ServiceSupervisor.get_pid(symbol) do # <= module added nil -> - Logger.warn("Trading on #{symbol} already stopped") + Logger.warning("Trading on #{symbol} already stopped") {:ok, _settings} = Core.ServiceSupervisor.update_status(symbol, "off") # ^^^ updated name + module @@ -502,7 +502,7 @@ Let's look at updated functions: # ^^^ args used pid -> - Logger.warn("#{worker_module} worker for #{symbol} already started") + Logger.warning("#{worker_module} worker for #{symbol} already started") # ^^^ dynamic text {:ok, _settings} = update_status(symbol, "on", repo, schema) {:ok, pid} @@ -514,7 +514,7 @@ Let's look at updated functions: when is_binary(symbol) do case get_pid(worker_module, symbol) do # <= worker_module passed nil -> - Logger.warn("#{worker_module} worker for #{symbol} already stopped") + Logger.warning("#{worker_module} worker for #{symbol} already stopped") # ^^^ dynamic text {:ok, _settings} = update_status(symbol, "off", repo, schema) @@ -615,7 +615,7 @@ That finishes the 3rd round of updates inside the `Core.ServiceSupervisor` modul def shutdown_worker(symbol) when is_binary(symbol) do case Core.ServiceSupervisor.get_pid(Naive.SymbolSupervisor, symbol) do # <= arg added nil -> - Logger.warn("#{Naive.SymbolSupervisor} worker for #{symbol} already stopped") + Logger.warning("#{Naive.SymbolSupervisor} worker for #{symbol} already stopped") # ^^^ updated {:ok, _settings} = @@ -862,7 +862,7 @@ As those will get compiled and "pasted" into the `Naive.DynamicSymbolSupervisor` def shutdown_worker(symbol) when is_binary(symbol) do case get_pid(symbol) do # <= macro provided function nil -> - Logger.warn("#{Naive.SymbolSupervisor} worker for #{symbol} already stopped") + Logger.warning("#{Naive.SymbolSupervisor} worker for #{symbol} already stopped") {:ok, _settings} = update_status(symbol, "off") # <= macro provided function _pid -> diff --git a/14-store-trade-events-and-orders.Rmd b/14-store-trade-events-and-orders.Rmd index f26f60e..e63cb0e 100644 --- a/14-store-trade-events-and-orders.Rmd +++ b/14-store-trade-events-and-orders.Rmd @@ -50,6 +50,7 @@ We can now follow similar steps as previously and add required dependencies (lik # /apps/data_warehouse/mix.exs defp deps do [ + {:binance, "~> 1.0"}, {:ecto_sql, "~> 3.0"}, {:ecto_enum, "~> 1.4"}, {:phoenix_pubsub, "~> 2.0"}, @@ -59,7 +60,7 @@ We can now follow similar steps as previously and add required dependencies (lik end ``` -Additionally, we added the `phoenix_pubsub` module to be able to subscribe to the PubSub topic and the `streamer` application to be able to use its `Streamer.Binance.TradeEvent` struct. +Additionally, we added the `phoenix_pubsub`(to subscribe to the PubSub topic), the `streamer` application(to use its `Streamer.Binance.TradeEvent` struct) and the `binance` package(to pattern match it's structs). We can now jump back to the terminal to install added dependencies and generate a new `Ecto.Repo` module: @@ -800,7 +801,7 @@ Last function in this module will be `stop_worker/1` which uses private `stop_ch defp stop_child(args) do case Registry.lookup(@registry, args) do [{pid, _}] -> DynamicSupervisor.terminate_child(__MODULE__, pid) - _ -> Logger.warn("Unable to locate process assigned to #{inspect(args)}") + _ -> Logger.warning("Unable to locate process assigned to #{inspect(args)}") end end ``` diff --git a/16-end-to-end-testing.Rmd b/16-end-to-end-testing.Rmd index 3f12a8e..034598f 100644 --- a/16-end-to-end-testing.Rmd +++ b/16-end-to-end-testing.Rmd @@ -586,8 +586,8 @@ defmodule Core.Struct.TradeEvent do As we moved the `TradeEvent` struct over to the `Core` application, we need to: * update all places that reference the `Streamer.Binance.TradeEvent` to `Core.Struct.TradeEvent` -* add the `core` to the dependencies list of the `streamer` application -* remove the `streamer` from the dependencies list of all apps in the umbrella +* add the `core` to the dependencies lists of all apps in the umbrella +* remove the `streamer` from the dependencies lists of all apps in the umbrella diff --git a/18-functional-elixir.Rmd b/18-functional-elixir.Rmd index 8af7f77..86530cc 100644 --- a/18-functional-elixir.Rmd +++ b/18-functional-elixir.Rmd @@ -295,53 +295,56 @@ The second function deals with the race condition when multiple transactions fil }, sell_order: %Binance.OrderResponse{} } - ) do + ) + when is_number(order_id) do :skip end ``` -### Place a sell order rules +### Fetch the buy order rules -We will follow the same logic for the 3rd clause of the `generate_decision/2` function. We will leave only the sell price calculation as it's pure and return a tuple together with the decision: +For the 3th clause, we will return only an atom as there's no pure logic besides the pattern-match in the header itself: ```{r, engine = 'elixir', eval = FALSE} # /apps/naive/lib/naive/strategy.ex # the third clause def generate_decision( - %TradeEvent{}, + %TradeEvent{ + buyer_order_id: order_id + }, %State{ buy_order: %Binance.OrderResponse{ - status: "FILLED", - price: buy_price + order_id: order_id }, - sell_order: nil, - profit_interval: profit_interval, - tick_size: tick_size + sell_order: nil } - ) do - sell_price = calculate_sell_price(buy_price, profit_interval, tick_size) - {:place_sell_order, sell_price} + ) + when is_number(order_id) do + :fetch_buy_order end ``` -### Fetch the buy order rules +### Place a sell order rules -For the 4th clause, we will return only an atom as there's no pure logic besides the pattern-match in the header itself: +We will follow the same logic for the 4th clause of the `generate_decision/2` function. We will leave only the sell price calculation as it's pure and return a tuple together with the decision: ```{r, engine = 'elixir', eval = FALSE} # /apps/naive/lib/naive/strategy.ex # the fourth clause def generate_decision( - %TradeEvent{ - buyer_order_id: order_id - }, + %TradeEvent{}, %State{ buy_order: %Binance.OrderResponse{ - order_id: order_id - } + status: "FILLED", + price: buy_price + }, + sell_order: nil, + profit_interval: profit_interval, + tick_size: tick_size } ) do - :fetch_buy_order + sell_price = calculate_sell_price(buy_price, profit_interval, tick_size) + {:place_sell_order, sell_price} end ``` diff --git a/19-idiomatic-elixir.Rmd b/19-idiomatic-elixir.Rmd index 23224cc..a70319b 100644 --- a/19-idiomatic-elixir.Rmd +++ b/19-idiomatic-elixir.Rmd @@ -81,6 +81,16 @@ As each worker will need to subscribe to the PubSub's `"TRADE_EVENTS:#{symbol}"` Following the pattern established by the `Naive.Trader`, we use the module's attributes(with values based on the configuration) instead of hardcoded module names. +Additionally, we used the `Core.PubSub` module(and we will use other `core` module's structs down below) so we need to add the `core` application to the dependencies list of the `indicator` application: + +```{r, engine = 'elixir', eval = FALSE} +# /apps/indicator/mix.exs + defp deps do + [ + {:core, in_umbrella: true} # <= added + ... +``` + As we subscribed to the PubSub, we need to provide a callback that will handle the incoming trade events: ```{r, engine = 'elixir', eval = FALSE} @@ -93,8 +103,6 @@ As we subscribed to the PubSub, we need to provide a callback that will handle t end ``` -\newpage - To avoid mixing our business logic with the GenServer boilerplate(as discussed in the last chapter), we will place it in a new module. First, we need to create a new file `/apps/indicator/lib/indicator/ohlc.ex` and the `Indicator.Ohlc` module inside it: ```{r, engine = 'elixir', eval = FALSE} @@ -301,7 +309,7 @@ We could continue with this exercise, add a Registry to be able to stop the indi ## Idiomatic solution -What we will focus on is the **usage** of processes(in our case the GenServers) in our solution. We've split our logic between multiple processes, each aggregating a single timeframe. All of those processes work in the same way. They subscribe to the PubSub topic, merge the incoming data into OHLC structs, and potentially broadcast them. The only difference is the timeframe that they use for merging data. +What we will focus on is the **usage of processes**(in our case the GenServers) in our solution. We've split our logic between multiple processes, each aggregating a single timeframe. All of those processes work in the same way. They subscribe to the PubSub topic, merge the incoming data into OHLC structs, and potentially broadcast them. The only difference is the timeframe that they use for merging data. The solution feels very clean, but also we are using multiple processes to aggregate data for each symbol. In a situation like this, we should always ask ourselves: diff --git a/20-idiomatic-trading-strategy.Rmd b/20-idiomatic-trading-strategy.Rmd index 8cd0ee2..770d6f1 100644 --- a/20-idiomatic-trading-strategy.Rmd +++ b/20-idiomatic-trading-strategy.Rmd @@ -403,7 +403,7 @@ At this moment, the `generate_decisions/4` can look like overengineered `Enum.ma \newpage -It's important to note that we are now passing four arguments into the `generate_decision` function - we added `current_positions` and `settings` - those will be required in the further updates as it was mentioned above. At this moment though, we will update the `generate_decision/2` clauses to include two additional arguments to **all** clauses: +It's important to note that we are now passing four arguments into the `generate_decision` function - we added `current_positions` and `settings` - those will be required in the further updates as it was mentioned above. At this moment though, we will update **all** the `generate_decision/2` clauses to include two additional arguments: ```{r, engine = 'elixir', eval = FALSE} # /apps/naive/lib/naive/strategy.ex @@ -592,7 +592,7 @@ Now, the Trader will handle updating the settings, which we will add next, but b # /apps/naive/lib/naive/strategy.ex def update_status(symbol, status) # <= updated to public when is_binary(symbol) and is_binary(status) do - @repo.get_by(Settings, symbol: symbol) <= updated to use @repo + @repo.get_by(Settings, symbol: symbol) # <= updated to use @repo |> Ecto.Changeset.change(%{status: status}) |> @repo.update() # <= updated to use @repo end @@ -639,7 +639,7 @@ We can now move on to the `Naive.Trader` module, where we need to add a new `no ) _ -> - Logger.warn("Unable to locate trader process assigned to #{symbol}") + Logger.warning("Unable to locate trader process assigned to #{symbol}") {:error, :unable_to_locate_trader} end end @@ -727,7 +727,7 @@ As we added a new `:exit` decision that we need to handle inside the `generate_d ... ``` -Inside the recursive function, we are skipping all the positions that ended up with the `:exit` decisions. This will slowly cause the list of positions to drain to an empty list, which will cause the `parse_results/1` function to fail(as it expects non-empty list). We will add a new first clause to match the empty list of positions and return the `:exit` atom.: +Inside the recursive function, we are skipping all the positions that ended up with the `:exit` decisions. This will slowly cause the list of positions to drain to an empty list, which will cause the `parse_results/1` function to fail(as it expects non-empty list). We will add a new first clause to match the empty list of positions and return the `:exit` atom: ```{r, engine = 'elixir', eval = FALSE} # /apps/naive/lib/naive/strategy.ex diff --git a/21-layers-of-abstraction.Rmd b/21-layers-of-abstraction.Rmd index 4ff5ffd..d4e5c25 100644 --- a/21-layers-of-abstraction.Rmd +++ b/21-layers-of-abstraction.Rmd @@ -558,7 +558,7 @@ The other places we use the exchange are seed scripts that we need to update. Fi ```{r, engine = 'elixir', eval = FALSE} # /apps/naive/priv/seed_settings.exs -exchange_client = Application.get_env(:naive, :exchange_client) +exchange_client = Application.compile_env(:naive, :exchange_client) ... {:ok, symbols} = exchange_client.fetch_symbols() ... diff --git a/docs/404.html b/docs/404.html index ad5aa2f..17a3bc7 100644 --- a/docs/404.html +++ b/docs/404.html @@ -6,7 +6,7 @@ Page not found | Hands-on Elixir & OTP: Cryptocurrency trading bot - + @@ -127,6 +127,10 @@ code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */ + @@ -361,8 +365,8 @@