defmodule OHLCHelper do @moduledoc """ OHLC Helper module containing all the helper functions. """ @timeframes [minute: 60, hour: 3600, day: 86_400, week: 604_800] @doc """ Returns all available timeframes in seconds. """ @spec get_timeframes() :: list() def get_timeframes() do @timeframes end @deprecated "Will be removed in v2.x. Use OHLCFactory.gen_empty_candle/1 instead." @doc """ Generates and returns empty candle. If provided with timeframe stime and etime will be generated based on current time. """ @spec generate_empty_candle(OHLC.timeframe() | nil) :: OHLC.candle() def generate_empty_candle(timeframe \\ nil) do OHLCFactory.gen_empty_candle(timeframe) end @doc """ Helper function for formatting the value to float. """ @spec format_to_float(any) :: float | false def format_to_float(value) when is_binary(value) do {float, _} = value |> String.replace(",", ".") |> String.trim() |> Float.parse() float end def format_to_float(value) when is_number(value) or is_integer(value), do: value / 1 def format_to_float(value) when is_float(value), do: value def format_to_float(_), do: false @doc """ Gets the rounded timestamp based on the timeframe. Parameters: - `timestamp` - Unix timestamp which will be rounded. - `timeframe` - Timeframe used for rounding the timestamp. Available values are: `:minute`, `:hour`, `:day`, `:week` - `opts` - Options for rounding the timestamp. Available values are: - `{:format, :stamp | :struct}` - Returned value will be unix timestamp or DateTime struct. - `{:type, :down | :up | :jump}` - Timestamp will be rounded up, down or jump to the next time cycle. Default is `:up`. """ @spec get_time_rounded(number(), OHLC.timeframe(), list() | nil) :: number() | %DateTime{} def get_time_rounded(timestamp, timeframe, opts \\ []) do timestamp = timestamp |> format_to_float() |> round() {:ok, time_struct} = DateTime.from_unix(timestamp) candle_time = %DateTime{ year: 00, month: 00, day: 00, hour: 00, minute: 00, second: 00, time_zone: time_struct.time_zone, zone_abbr: time_struct.zone_abbr, utc_offset: time_struct.utc_offset, std_offset: time_struct.std_offset } rounded_time = case timeframe do :minute -> time_minute_worker(time_struct, candle_time, opts) :hour -> time_hour_worker(time_struct, candle_time, opts) :day -> time_day_worker(time_struct, candle_time, opts) :week -> time_week_worker(time_struct, candle_time, opts) end case opts[:format] do :stamp -> DateTime.to_unix(rounded_time) :struct -> rounded_time _ -> DateTime.to_unix(rounded_time) end end @deprecated "Will be removed in v2.x. Use OHLCFactory.gen_trades/1 instead." @doc """ Generates trades from provided arguments. Parameters: - `timeframe` - Timeframe used for rounding the timestamp. Available values are: `:minute`, `:hour`, `:day`, `:week` - `min_price` - The minimum price on the generated trades - `max_price` - The maximum price on the generated trades - `volume` - The volume each trade has - `timeframe_multiplier` - If you'd like to generate less trades per candle then you can increase the size of the timeframe_divider parameter(1-100) otherwise leave empty. - `timeframe_divider` - Is used for generating multiple candles of the same timeframe. """ @spec gen_trades(OHLC.timeframe(), number(), number(), number(), integer(), integer()) :: list() def gen_trades( timeframe, min_price, max_price, volume, timeframe_multiplier \\ 1, timeframe_divider \\ 1 ) do OHLCFactory.gen_trades( timeframe: timeframe, min_price: min_price, max_price: max_price, volume: volume, timeframe_multiplier: timeframe_multiplier, timeframe_divider: timeframe_divider ) end @doc """ Calculates the total volume from trades. """ @spec trades_total_volume(OHLC.trades()) :: float def trades_total_volume(trades) do Enum.reduce(trades, fn trade, acc -> volume_formatted = format_to_float(trade[:volume]) if is_float(acc) do acc + volume_formatted else format_to_float(acc[:volume]) + volume_formatted end end) |> Float.round(4) end @doc """ Gets the current candle type(pullish or bearish). Returns `:bullish` - if close price > open price. `bearish` - if previous close price <= open price. """ @spec get_candle_type(number(), number()) :: :bullish | :bearish def get_candle_type(open, close) do if close > open, do: :bullish, else: :bearish end @doc """ Validates the data used for generating the OHLC candles. Accepts lists of candles, trades or both. """ @spec validate_data(OHLC.candles() | nil, OHLC.trades() | nil) :: :ok | {:error, atom()} def validate_data(candles \\ [], trades \\ []) do case validate_candles(candles) do {:error, candles_error_msg} -> {:error, candles_error_msg} :ok -> trades_validated = validate_trades(trades) case trades_validated do {:error, trades_error_msg} -> {:error, trades_error_msg} :ok -> :ok end end end defp time_minute_worker(time_struct, unfinished_time_struct, opts) do timeframe_secs = get_timeframes()[:minute] cond do opts[:type] === :down -> Map.put(time_struct, :second, 00) opts[:type] === :up or opts[:type] === nil -> Map.put(time_struct, :second, 59) opts[:type] === :jump -> DateTime.add(time_struct, timeframe_secs, :second) |> Map.put(:second, 00) end |> set_worked_time(unfinished_time_struct) end defp time_hour_worker(time_struct, unfinished_time_struct, opts) do timeframe_secs = get_timeframes()[:hour] cond do opts[:type] === :down -> Map.put(time_struct, :second, 00) |> Map.put(:minute, 00) opts[:type] === :up or opts[:type] === nil -> Map.put(time_struct, :second, 59) |> Map.put(:minute, 59) opts[:type] === :jump -> DateTime.add(time_struct, timeframe_secs, :second) |> Map.put(:second, 00) |> Map.put(:minute, 00) end |> set_worked_time(unfinished_time_struct) end defp time_day_worker(time_struct, unfinished_time_struct, opts) do timeframe_secs = get_timeframes()[:day] cond do opts[:type] === :down -> Map.put(time_struct, :second, 00) |> Map.put(:minute, 00) |> Map.put(:hour, 00) opts[:type] === :up or opts[:type] === nil -> Map.put(time_struct, :second, 59) |> Map.put(:minute, 59) |> Map.put(:hour, 23) opts[:type] === :jump -> DateTime.add(time_struct, timeframe_secs, :second) |> Map.put(:second, 00) |> Map.put(:minute, 00) |> Map.put(:hour, 00) end |> set_worked_time(unfinished_time_struct) end defp time_week_worker(time_struct, unfinished_time_struct, opts) do timeframe_secs = get_timeframes()[:week] cond do opts[:type] === :down -> Date.beginning_of_week(time_struct) |> Map.put(:second, 00) |> Map.put(:minute, 00) |> Map.put(:hour, 00) opts[:type] === :up or opts[:type] === nil -> Date.end_of_week(time_struct) |> Map.put(:second, 59) |> Map.put(:minute, 59) |> Map.put(:hour, 23) opts[:type] === :jump -> DateTime.add(time_struct, timeframe_secs, :second) |> Date.beginning_of_week() |> Map.put(:second, 00) |> Map.put(:minute, 00) |> Map.put(:hour, 00) end |> set_worked_time(unfinished_time_struct) end defp set_worked_time(worked_timestruct, def_timestruct) do %{ def_timestruct | year: worked_timestruct.year, month: worked_timestruct.month, day: worked_timestruct.day, hour: worked_timestruct.hour, minute: worked_timestruct.minute, second: worked_timestruct.second } end defp validate_candles(candles) do empty_candle = OHLCFactory.gen_empty_candle() data_validated = Enum.all?(candles, fn candle -> if is_map(candle) do Enum.all?(empty_candle, fn el -> Map.has_key?(candle, el |> elem(0)) end) else false end end) if data_validated do :ok else {:error, :candle_invalid} end end defp validate_trades(trades, prev_etime \\ nil) defp validate_trades([trades_head | trades_body], prev_etime) do trade_fields = [:price, :volume, :time] keys_validated = Enum.all?(trade_fields, &trades_head[&1]) case keys_validated do true -> trade_data_validated = validate_trade_data(trades_head, prev_etime) case trade_data_validated do :ok -> validate_trades(trades_body, trades_head[:time]) {:error, msg} -> {:error, msg} end false -> {:error, :invalid_trades} end end defp validate_trades([], _prev_etime) do :ok end defp validate_trade_data(trade, prev_etime) do price_validation = is_float(format_to_float(trade[:price])) volume_validation = is_float(format_to_float(trade[:volume])) time_validation = is_float(format_to_float(trade[:time])) etime_greater = cond do prev_etime === nil -> true time_validation -> format_to_float(trade[:time]) >= format_to_float(prev_etime) || false true -> false end cond do !price_validation -> {:error, :invalid_price} !volume_validation -> {:error, :invalid_volume} !time_validation -> {:error, :invalid_time} !etime_greater -> {:error, :invalid_candle_order} true -> :ok end end end