defmodule OHLCFactory do @moduledoc """ This module provides functionality to generate random or defined OHLC candles or trades which can be used for testing, demoing etc. """ # 2021-22-03 00:00:00 UTC +0 @base_timestamp 1_616_371_200 @base_volume 9 @base_trades 9 @base_price 9 @base_price_change_percentage 2 @doc """ Generates trades from provided arguments. Options: - `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(keyword() | nil) :: list() def gen_trades(opts \\ []) do timeframe = Keyword.get(opts, :timeframe, :minute) min_price = Keyword.get(opts, :min_price, 10) max_price = Keyword.get(opts, :max_price, 20) volume = Keyword.get(opts, :volume, 10) timeframe_multiplier = Keyword.get(opts, :timeframe_multiplier, 1) timeframe_divider = Keyword.get(opts, :timeframe_divider, 1) timeframe_secs = OHLCHelper.get_timeframes()[timeframe] price_range = max_price - min_price if is_float(price_range), do: Float.round(price_range, 4), else: price_range timestamp_multipled = @base_timestamp + timeframe_secs * timeframe_multiplier items_to_loop = ((timeframe_secs - 1) / timeframe_divider) |> trunc() Enum.map(1..items_to_loop, fn numb -> numb_multiplied = numb * timeframe_divider price = cond do numb === 1 -> max_price numb === items_to_loop -> min_price true -> (price_range / numb_multiplied + min_price) |> Float.round(4) end price = (is_float(price) && Float.round(price, 4)) || price volume = (is_float(volume) && Float.round(volume, 4)) || volume [ price: price, volume: volume, time: timestamp_multipled + numb_multiplied ] end) end @doc """ Generates empty OHLC candle. If provided with timeframe stime and etime will be generated based on current time. """ @spec gen_empty_candle(OHLC.timeframe() | nil) :: OHLC.candle() def gen_empty_candle(timeframe \\ nil) do {stime, etime} = case timeframe do nil -> {0, 0} _ -> curr_timestamp = DateTime.utc_now() |> DateTime.to_unix() { OHLCHelper.get_time_rounded(curr_timestamp, timeframe, type: :down), OHLCHelper.get_time_rounded(curr_timestamp, timeframe, type: :up) } end %{ :open => 0.0, :high => 0.0, :low => 0.0, :close => 0.0, :volume => 0.0, :trades => 0, :stime => stime, :etime => etime, :type => nil, :processed => false } end @doc """ Generates candles based on parameters provided. Parameters: - timeframe - See available timeframes `OHLCHelper.get_timeframes`. - amount - Amount of candles to generate. Must be bigger than 0. Available options: - `:base_price` - Base price to use when generating candles. Defaults to 9. - `:price_direction` - Generated candles can increase or decrease in price. `:increase`, `:decrease` or `:rand` which is used by default. - `:price_change_percentage` - With each new candle we dynamically update the price for each new candle. This price can be higher than the previous candle or lesser. With this option you can choose the percentage change for each new candle. Defaults to 2% change with each new candle. """ @spec gen_candles(OHLC.timeframe(), number(), keyword() | nil) :: list() def gen_candles(timeframe, amount, opts \\ []) do base_stime = DateTime.utc_now() |> DateTime.to_unix() |> OHLCHelper.get_time_rounded(timeframe, type: :down) change_seconds = OHLCHelper.get_timeframes()[timeframe] base_price = Keyword.get(opts, :base_price, @base_price) price_change_percentage = Keyword.get(opts, :price_change_percentage, @base_price_change_percentage) price_dir = Keyword.get(opts, :price_direction, :rand) Enum.map_reduce(amount..1, base_price, fn counter, last_close_price -> updated_stime = base_stime - counter * change_seconds price_dir = if price_dir === :rand, do: Enum.random([:increase, :decrease]), else: price_dir {open, high, low, close, type} = gen_ohlc_data(price_dir, last_close_price, price_change_percentage) etime = OHLCHelper.get_time_rounded(updated_stime, timeframe) { gen_empty_candle(timeframe) |> Map.put(:volume, @base_volume) |> Map.put(:trades, @base_trades) |> Map.put(:stime, updated_stime) |> Map.put(:etime, etime) |> Map.put(:open, open) |> Map.put(:close, close) |> Map.put(:high, high) |> Map.put(:low, low) |> Map.put(:type, type), close } end) |> elem(0) end defp percentage_change(initial_val, percentage, type) do case type do :increase -> initial_val / 100 * percentage + initial_val :decrease -> initial_val * (1.0 - percentage / 100) end end defp gen_ohlc_data(price_direction, base_price, price_change_percentage) do ohlc = { base_price, percentage_change(base_price, price_change_percentage + 1, :increase), percentage_change(base_price, price_change_percentage + 1, :decrease), percentage_change(base_price, price_change_percentage, price_direction) } case price_direction do :increase -> Tuple.append(ohlc, :bullish) :decrease -> Tuple.append(ohlc, :bearish) end end end