Skip to content

Commit 4aeb081

Browse files
authored
Chapter 19 (#21)
* working through chapter 19 * finished chapter 19 * self code review against franton code
1 parent 4837942 commit 4aeb081

11 files changed

Lines changed: 239 additions & 0 deletions

File tree

apps/indicator/.formatter.exs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Used by "mix format"
2+
[
3+
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
4+
]

apps/indicator/.gitignore

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# The directory Mix will write compiled artifacts to.
2+
/_build/
3+
4+
# If you run "mix test --cover", coverage assets end up here.
5+
/cover/
6+
7+
# The directory Mix downloads your dependencies sources to.
8+
/deps/
9+
10+
# Where third-party dependencies like ExDoc output generated docs.
11+
/doc/
12+
13+
# Ignore .fetch files in case you like to edit your project deps locally.
14+
/.fetch
15+
16+
# If the VM crashes, it generates a dump, let's ignore it too.
17+
erl_crash.dump
18+
19+
# Also ignore archive artifacts (built via "mix archive.build").
20+
*.ez
21+
22+
# Ignore package tarball (built via "mix hex.build").
23+
indicator-*.tar
24+
25+
# Temporary files, for example, from tests.
26+
/tmp/

apps/indicator/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Indicator
2+
3+
**TODO: Add description**
4+
5+
## Installation
6+
7+
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
8+
by adding `indicator` to your list of dependencies in `mix.exs`:
9+
10+
```elixir
11+
def deps do
12+
[
13+
{:indicator, "~> 0.1.0"}
14+
]
15+
end
16+
```
17+
18+
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
19+
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
20+
be found at <https://hexdocs.pm/indicator>.
21+

apps/indicator/lib/indicator.ex

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
defmodule Indicator do
2+
@moduledoc """
3+
Documentation for `Indicator`.
4+
"""
5+
def aggregate_ohlcs(symbol) do
6+
DynamicSupervisor.start_child(
7+
Indicator.DynamicSupervisor,
8+
{Indicator.Ohlc.Worker, symbol}
9+
)
10+
end
11+
end
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
defmodule Indicator.Application do
2+
# See https://hexdocs.pm/elixir/Application.html
3+
# for more information on OTP Applications
4+
@moduledoc false
5+
6+
use Application
7+
8+
@impl true
9+
def start(_type, _args) do
10+
children = [
11+
{DynamicSupervisor, strategy: :one_for_one, name: Indicator.DynamicSupervisor}
12+
]
13+
14+
# See https://hexdocs.pm/elixir/Supervisor.html
15+
# for other strategies and supported options
16+
opts = [strategy: :one_for_one, name: Indicator.Supervisor]
17+
Supervisor.start_link(children, opts)
18+
end
19+
end
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
defmodule Indicator.Ohlc do
2+
require Logger
3+
alias Core.Struct.TradeEvent
4+
5+
@pubsub_client Application.compile_env(:core, :pubsub_client)
6+
@enforce_keys [:symbol, :start_time, :duration]
7+
defstruct [:symbol, :start_time, :duration, :open, :high, :low, :close]
8+
9+
def process([_ | _] = ohlcs, %TradeEvent{} = trade_event) do
10+
{old_ohlcs, new_ohlcs} = merge_prices(ohlcs, trade_event.price, trade_event.trade_time)
11+
Enum.each(old_ohlcs, &maybe_broadcast/1)
12+
new_ohlcs
13+
end
14+
15+
def process(symbol, %TradeEvent{} = trade_event) do
16+
generate_ohlcs(symbol, trade_event.price, trade_event.trade_time)
17+
end
18+
19+
def merge_prices(ohlcs, price, trade_time) do
20+
results = ohlcs |> Enum.map(&merge_price(&1, price, trade_time))
21+
22+
{
23+
results |> Enum.map(&elem(&1, 0)) |> Enum.filter(& &1),
24+
results |> Enum.map(&elem(&1, 1))
25+
}
26+
end
27+
28+
def merge_price(%__MODULE__{} = ohlc, price, trade_time) do
29+
if within_current_timeframe(ohlc.start_time, ohlc.duration, trade_time) do
30+
{nil, %{ohlc | low: min(ohlc.low, price), high: max(ohlc.high, price), close: price}}
31+
else
32+
{ohlc, generate_ohlc(ohlc.symbol, ohlc.duration, price, trade_time)}
33+
end
34+
end
35+
36+
def within_current_timeframe(start_time, duration, trade_time) do
37+
end_time = start_time + duration * 60
38+
trade_time = div(trade_time, 1000)
39+
start_time <= trade_time && trade_time < end_time
40+
end
41+
42+
def generate_ohlcs(symbol, price, trade_time) do
43+
[1, 5, 15, 60, 4 * 60, 24 * 60]
44+
|> Enum.map(
45+
&generate_ohlc(
46+
symbol,
47+
&1,
48+
price,
49+
trade_time
50+
)
51+
)
52+
end
53+
54+
def generate_ohlc(symbol, duration, price, trade_time) do
55+
# start_time = div(div(div(trade_time, 1000), 60), duration)
56+
start_time = trade_time |> div(1000) |> div(60) |> div(duration)
57+
58+
%__MODULE__{
59+
symbol: symbol,
60+
start_time: start_time,
61+
duration: duration,
62+
open: price,
63+
high: price,
64+
low: price,
65+
close: price
66+
}
67+
end
68+
69+
defp maybe_broadcast(nil), do: :ok
70+
71+
defp maybe_broadcast(%__MODULE__{} = ohlc) do
72+
Logger.debug("Broadcasting OHLC: #{inspect(ohlc)}")
73+
74+
@pubsub_client.broadcast(
75+
Core.PubSub,
76+
"OHLC:#{ohlc.symbol}",
77+
ohlc
78+
)
79+
end
80+
end
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
defmodule Indicator.Ohlc.Worker do
2+
use GenServer
3+
require Logger
4+
alias Core.Struct.TradeEvent
5+
6+
@logger Application.compile_env(:core, :logger)
7+
@pubsub_client Application.compile_env(:core, :pubsub_client)
8+
9+
def start_link(symbol) do
10+
GenServer.start_link(__MODULE__, symbol)
11+
end
12+
13+
def init(symbol) do
14+
symbol = String.upcase(symbol)
15+
@logger.debug("Initializing a new OHLC worker for #{symbol}")
16+
17+
@pubsub_client.subscribe(
18+
Core.PubSub,
19+
"TRADE_EVENTS:#{symbol}"
20+
)
21+
22+
{:ok, symbol}
23+
end
24+
25+
def handle_info(%TradeEvent{} = trade_event, ohlc) do
26+
{:noreply, Indicator.Ohlc.process(ohlc, trade_event)}
27+
end
28+
end

apps/indicator/mix.exs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
defmodule Indicator.MixProject do
2+
use Mix.Project
3+
4+
def project do
5+
[
6+
app: :indicator,
7+
version: "0.1.0",
8+
build_path: "../../_build",
9+
config_path: "../../config/config.exs",
10+
deps_path: "../../deps",
11+
lockfile: "../../mix.lock",
12+
elixir: "~> 1.17",
13+
start_permanent: Mix.env() == :prod,
14+
deps: deps()
15+
]
16+
end
17+
18+
# Run "mix help compile.app" to learn about applications.
19+
def application do
20+
[
21+
extra_applications: [:logger],
22+
mod: {Indicator.Application, []}
23+
]
24+
end
25+
26+
# Run "mix help deps" to learn about dependencies.
27+
defp deps do
28+
[
29+
{:core, in_umbrella: true},
30+
{:phoenix_pubsub, "~> 2.0"}
31+
]
32+
end
33+
end
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
defmodule IndicatorTest do
2+
use ExUnit.Case
3+
doctest Indicator
4+
5+
test "greets the world" do
6+
assert Indicator.hello() == :world
7+
end
8+
end
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ExUnit.start()

0 commit comments

Comments
 (0)