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()