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