Powered by AppSignal & Oban Pro

PETR4 — Ticks Intraday B3

inferenz.livemd

PETR4 — Ticks Intraday B3

Mix.install([
  {:req, "~> 0.5"},
  {:vega_lite, "~> 0.1.9"},
  {:kino_vega_lite, "~> 0.1.13"},
  {:kino, "~> 0.15"},
  {:tzdata, "~> 1.1"}
])

Application.put_env(:elixir, :time_zone_database, Tzdata.TimeZoneDatabase)

Config

ticker   = "PETR4"
range    = "max"   # todos os dados históricos disponíveis
interval = "1d"    # candle diário

Buscar dados da B3 (Yahoo Finance)

symbol  = "#{String.upcase(ticker)}.SA"
url     = "https://query1.finance.yahoo.com/v8/finance/chart/#{symbol}"
headers = [{"user-agent", "Mozilla/5.0"}]

response = Req.get!(url, params: [range: range, interval: interval], headers: headers)

IO.puts("Status: #{response.status}")
response.status

Parse dos ticks

body   = response.body
result = get_in(body, ["chart", "result"]) |> List.first()
meta   = result["meta"]
timestamps = result["timestamp"] || []
quotes     = get_in(result, ["indicators", "quote"]) |> List.first() || %{}

ticks =
  timestamps
  |> Enum.with_index()
  |> Enum.map(fn {ts, i} ->
    open  = Enum.at(quotes["open"]  || [], i)
    close = Enum.at(quotes["close"] || [], i)

    dt =
      ts
      |> DateTime.from_unix!()
      |> DateTime.shift_zone!("America/Sao_Paulo")

    %{
      timestamp: Calendar.strftime(dt, "%d/%m/%Y"),
      open:      open  && Float.round(open  * 1.0, 2),
      high:      Enum.at(quotes["high"] || [], i) |> then(&(&1 && Float.round(&1 * 1.0, 2))),
      low:       Enum.at(quotes["low"]  || [], i) |> then(&(&1 && Float.round(&1 * 1.0, 2))),
      close:     close && Float.round(close * 1.0, 2),
      volume:    Enum.at(quotes["volume"] || [], i) || 0,
      color:     if((close || 0) >= (open || 0), do: "#22c55e", else: "#ef4444")
    }
  end)
  |> Enum.reject(&(is_nil(&1.open) or is_nil(&1.close)))

IO.puts("#{length(ticks)} ticks carregados para #{ticker} (#{interval})")
ticks |> Enum.take(5) |> Kino.DataTable.new()

Tabela completa de ticks

Kino.DataTable.new(ticks)

Gráfico de Candles (OHLC)

alias VegaLite, as: Vl

wick =
  Vl.new()
  |> Vl.mark(:rule, stroke_width: 1)
  |> Vl.encode_field(:x, "timestamp",
    type: :ordinal,
    axis: [label_angle: -60, label_font_size: 8, tick_min_step: 5, title: nil]
  )
  |> Vl.encode_field(:y, "low",
    type: :quantitative,
    scale: [zero: false],
    axis: [title: "Preço (R$)"]
  )
  |> Vl.encode_field(:y2, "high")
  |> Vl.encode_field(:color, "color", type: :nominal, scale: nil, legend: nil)

body_layer =
  Vl.new()
  |> Vl.mark(:bar, width: [band: 0.6])
  |> Vl.encode_field(:x, "timestamp", type: :ordinal)
  |> Vl.encode_field(:y, "open",  type: :quantitative, scale: [zero: false])
  |> Vl.encode_field(:y2, "close")
  |> Vl.encode_field(:color, "color", type: :nominal, scale: nil, legend: nil)

Vl.new(width: :container, height: 320, title: "#{ticker} — Candles #{interval}", background: nil)
|> Vl.config(view: [stroke: nil])
|> Vl.data_from_values(ticks)
|> Vl.layers([wick, body_layer])
|> Kino.VegaLite.new()

Gráfico de Volume

alias VegaLite, as: Vl

Vl.new(width: :container, height: 100, title: "Volume", background: nil)
|> Vl.config(view: [stroke: nil])
|> Vl.data_from_values(ticks)
|> Vl.mark(:bar, color: "#94a3b8")
|> Vl.encode_field(:x, "timestamp",
  type: :ordinal,
  axis: [labels: false, ticks: false, title: nil]
)
|> Vl.encode_field(:y, "volume",
  type: :quantitative,
  axis: [title: "Volume"]
)
|> Kino.VegaLite.new()

Estatísticas

prices = Enum.map(ticks, & &1.close)

[%{
  total_ticks:  length(ticks),
  abertura:     ticks |> List.first() |> then(& &1[:open]),
  fechamento:   ticks |> List.last()  |> then(& &1[:close]),
  maxima:       Enum.max(prices),
  minima:       Enum.min(prices),
  media:        Float.round(Enum.sum(prices) / length(prices), 2),
  volume_total: Enum.sum(Enum.map(ticks, & &1.volume))
}]
|> Kino.DataTable.new()