Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ohlc scale #94

Open
wants to merge 24 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8f800e7
fix body_width to match in pixels
DaTrader Jan 27, 2024
ef6f5dd
add padding
DaTrader Jan 28, 2024
55d0446
optimize fixed spacing
DaTrader Feb 4, 2024
61351f4
introduce steps in TimeScale display
DaTrader Feb 4, 2024
11e8cf2
fix step computation;
DaTrader Feb 4, 2024
c1591bf
remove newly added but unused function
DaTrader Feb 4, 2024
59cd74f
simple-format timeframe ticks;
DaTrader Feb 4, 2024
47803b5
format
DaTrader Feb 4, 2024
47afca1
make reuse of existing tick_interval explicit so tests can pass
DaTrader Feb 4, 2024
93620a4
keep only data that fits in and trim the rest to match the x-axis ticks
DaTrader Feb 11, 2024
0d62a26
plot only the inner halves of first and last candle/bar;
DaTrader Feb 11, 2024
d818abb
format
DaTrader Feb 11, 2024
453a7af
make candle left and right halves equally wide regardless of border
DaTrader Feb 18, 2024
179b2f0
apply timeframe-less drawing logic when timeframe not defined
DaTrader Feb 18, 2024
1b785cf
remove redundant dataset (keep just the mapping's one);
DaTrader Feb 25, 2024
4db508f
replace trimming with time (domain) window
DaTrader Mar 3, 2024
e8b740f
integrate overlays
DaTrader Mar 10, 2024
bf7a3a5
implement simple moving average
DaTrader Mar 10, 2024
2037f7f
make domain_min optionally a function
DaTrader Mar 17, 2024
eb0b743
avoid failing if data empty
DaTrader May 5, 2024
641b253
rename arg
DaTrader May 5, 2024
1887d5e
export interval computation function;
DaTrader May 5, 2024
b72b52b
format
DaTrader May 5, 2024
b36fbbe
add lag retrieval function
DaTrader May 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 54 additions & 20 deletions lib/chart/dataset.ex
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ defmodule Contex.Dataset do
Data is expected to be a list of tuples of the same size, a list of lists of same size, or a list of maps with the same keys.
Columns in map data are accessed by key. For lists of lists or tuples, if no headers are specified, columns are access by index.
"""
@spec new(list(row())) :: Contex.Dataset.t()
@spec new(list(row())) :: t()
def new(data) when is_list(data) do
%Dataset{headers: nil, data: data}
end
Expand All @@ -90,18 +90,27 @@ defmodule Contex.Dataset do
Data is expected to be a list of tuples of the same size or list of lists of same size. Headers provided with a list of maps
are ignored; column names from map data are inferred from the maps' keys.
"""
@spec new(list(row()), list(String.t())) :: Contex.Dataset.t()
@spec new(list(row()), list(String.t())) :: t()
def new(data, headers) when is_list(data) and is_list(headers) do
%Dataset{headers: headers, data: data}
end

@doc """
Updates data in the dataset.
The row structure is expected to remain the same.
"""
@spec update_data(t(), (row() -> row())) :: t()
def update_data(%Dataset{} = dataset, updater) do
%{dataset | data: updater.(dataset.data)}
end

@doc """
Optionally sets a title.

Not really used at the moment to be honest, but seemed like a good
idea at the time. Might come in handy when overlaying plots.
"""
@spec title(Contex.Dataset.t(), String.t()) :: Contex.Dataset.t()
@spec title(t(), String.t()) :: t()
def title(%Dataset{} = dataset, title) do
%{dataset | title: title}
end
Expand All @@ -112,15 +121,15 @@ defmodule Contex.Dataset do
Allows you to attach whatever you want to the dataset for later retrieval - e.g. information about where the
data came from.
"""
@spec meta(Contex.Dataset.t(), String.t()) :: Contex.Dataset.t()
@spec meta(t(), String.t()) :: t()
def meta(%Dataset{} = dataset, meta) do
%{dataset | meta: meta}
end

@doc """
Looks up the index for a given column name. Returns nil if not found.
"""
@spec column_index(Contex.Dataset.t(), column_name()) :: nil | column_name()
@spec column_index(t(), column_name()) :: nil | column_name()
def column_index(%Dataset{data: [first_row | _rest]}, column_name) when is_map(first_row) do
if Map.has_key?(first_row, column_name) do
column_name
Expand All @@ -144,7 +153,7 @@ defmodule Contex.Dataset do
Returns a list of the names of all of the columns in the dataset data (irrespective of
whether the column names are mapped to plot elements).
"""
@spec column_names(Contex.Dataset.t()) :: list(column_name())
@spec column_names(t()) :: list(column_name())
def column_names(%Dataset{headers: headers}) when not is_nil(headers), do: headers

def column_names(%Dataset{data: [first_row | _]}) when is_map(first_row) do
Expand All @@ -169,7 +178,7 @@ defmodule Contex.Dataset do
If there are no headers, or the index is outside the range of the headers
the requested index is returned.
"""
@spec column_name(Contex.Dataset.t(), integer() | any) :: column_name()
@spec column_name(t(), integer() | any) :: column_name()
def column_name(%Dataset{headers: headers} = _dataset, column_index)
when is_list(headers) and
is_integer(column_index) and
Expand All @@ -195,14 +204,11 @@ defmodule Contex.Dataset do
iex> category_accessor.(hd(data))
"Hippo"
"""
@spec value_fn(Contex.Dataset.t(), column_name()) :: (row() -> any)
def value_fn(%Dataset{data: [first_row | _]}, column_name)
when is_map(first_row) and is_binary(column_name) do
fn row -> row[column_name] end
end
@spec value_fn(t(), column_name()) :: (row() -> any)
def value_fn(dataset, column_name)

def value_fn(%Dataset{data: [first_row | _]}, column_name)
when is_map(first_row) and is_atom(column_name) do
when is_map(first_row) and (is_binary(column_name) or is_atom(column_name)) do
fn row -> row[column_name] end
end

Expand All @@ -223,16 +229,44 @@ defmodule Contex.Dataset do

def value_fn(_dataset, _column_name), do: fn _ -> nil end

@doc """
Returns the row with max value in the column.
"""
@spec max_row(t(), column_name()) :: row()
def max_row(%Dataset{} = dataset, column_name) do
accessor = value_fn(dataset, column_name)

Enum.reduce(dataset.data, {nil, nil}, fn row, {max, max_row} ->
val = accessor.(row)

if Utils.safe_max(val, max) == val do
{val, row}
else
{max, max_row}
end
end)
|> elem(1)
end

@doc """
Calculates the min and max value in the specified column

Options:
- filter: function filtering rows to take into account; takes a row and
returns a boolean
"""
@spec column_extents(Contex.Dataset.t(), column_name()) :: {any, any}
def column_extents(%Dataset{data: data} = dataset, column_name) do
@spec column_extents(t(), column_name(), keyword()) :: {any, any}
def column_extents(%Dataset{data: data} = dataset, column_name, opts \\ []) do
accessor = Dataset.value_fn(dataset, column_name)
filter = opts[:filter]

Enum.reduce(data, {nil, nil}, fn row, {min, max} ->
val = accessor.(row)
{Utils.safe_min(val, min), Utils.safe_max(val, max)}
if !filter or filter.(row) do
val = accessor.(row)
{Utils.safe_min(val, min), Utils.safe_max(val, max)}
else
{min, max}
end
end)
end

Expand All @@ -241,7 +275,7 @@ defmodule Contex.Dataset do

Looks through the rows and returns the first match it can find.
"""
@spec guess_column_type(Contex.Dataset.t(), column_name()) :: column_type()
@spec guess_column_type(t(), column_name()) :: column_type()
def guess_column_type(%Dataset{data: data} = dataset, column_name) do
accessor = Dataset.value_fn(dataset, column_name)

Expand All @@ -267,7 +301,7 @@ defmodule Contex.Dataset do
It is the equivalent of evaluating the extents of a calculated row where the calculating
is the sum of the values identified by column_names.
"""
@spec combined_column_extents(Contex.Dataset.t(), list(column_name())) :: {any(), any()}
@spec combined_column_extents(t(), list(column_name())) :: {any(), any()}
def combined_column_extents(%Dataset{data: data} = dataset, column_names) do
accessors =
Enum.map(column_names, fn column_name -> Dataset.value_fn(dataset, column_name) end)
Expand All @@ -291,7 +325,7 @@ defmodule Contex.Dataset do
Note that the unique values will maintain order of first detection
in the data.
"""
@spec unique_values(Contex.Dataset.t(), String.t() | integer()) :: [any]
@spec unique_values(t(), String.t() | integer()) :: [any]
def unique_values(%Dataset{data: data} = dataset, column_name) do
accessor = Dataset.value_fn(dataset, column_name)

Expand Down
97 changes: 97 additions & 0 deletions lib/chart/gallery/ohlc_candle_d1.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
data = [
{ ~N[2016-02-17 00:00:00], 1898.8, 1930.68, 1898.8, 1926.82, 2784188889},
{ ~N[2016-02-18 00:00:00], 1927.57, 1930, 1915.09, 1917.83, 2464716667},
{ ~N[2016-02-19 00:00:00], 1916.74, 1918.78, 1902.17, 1917.78, 2301583333},
{ ~N[2016-02-22 00:00:00], 1924.44, 1946.7, 1924.44, 1945.5, 2252616667},
{ ~N[2016-02-23 00:00:00], 1942.38, 1942.38, 1919.44, 1921.27, 2161472222},
{ ~N[2016-02-24 00:00:00], 1917.56, 1932.08, 1891, 1929.8, 2398472222},
{ ~N[2016-02-25 00:00:00], 1931.87, 1951.83, 1925.41, 1951.7, 2287894444},
{ ~N[2016-02-26 00:00:00], 1954.95, 1962.96, 1945.78, 1948.05, 2415838889},
{ ~N[2016-02-29 00:00:00], 1947.13, 1958.27, 1931.81, 1932.23, 2548988889},
{ ~N[2016-03-01 00:00:00], 1937.09, 1978.35, 1937.09, 1978.35, 2677638889},
{ ~N[2016-03-02 00:00:00], 1976.6, 1986.51, 1968.8, 1986.45, 2592561111},
{ ~N[2016-03-03 00:00:00], 1985.6, 1993.69, 1977.37, 1993.4, 2823166667},
{ ~N[2016-03-04 00:00:00], 1994.01, 2009.13, 1986.77, 1999.99, 3361072222},
{ ~N[2016-03-07 00:00:00], 1996.11, 2006.12, 1989.38, 2001.76, 2760100000},
{ ~N[2016-03-08 00:00:00], 1996.88, 1996.88, 1977.43, 1979.26, 2578694444},
{ ~N[2016-03-09 00:00:00], 1981.44, 1992.69, 1979.84, 1989.26, 2243400000},
{ ~N[2016-03-10 00:00:00], 1990.97, 2005.08, 1969.25, 1989.57, 2431550000},
{ ~N[2016-03-11 00:00:00], 1994.71, 2022.37, 1994.71, 2022.19, 2265900000},
{ ~N[2016-03-14 00:00:00], 2019.27, 2024.57, 2012.05, 2019.64, 1937694444},
{ ~N[2016-03-15 00:00:00], 2015.27, 2015.94, 2005.23, 2015.93, 1977933333},
{ ~N[2016-03-16 00:00:00], 2014.24, 2032.02, 2010.04, 2027.22, 2253900000},
{ ~N[2016-03-17 00:00:00], 2026.9, 2046.24, 2022.16, 2040.59, 2516933333},
{ ~N[2016-03-18 00:00:00], 2041.16, 2052.36, 2041.16, 2049.58, 3612855556},
{ ~N[2016-03-21 00:00:00], 2047.88, 2053.91, 2043.14, 2051.6, 1875888889},
{ ~N[2016-03-22 00:00:00], 2048.64, 2056.6, 2040.57, 2049.8, 1899144444},
{ ~N[2016-03-23 00:00:00], 2048.55, 2048.55, 2034.86, 2036.71, 2021950000},
{ ~N[2016-03-24 00:00:00], 2032.48, 2036.04, 2022.49, 2035.94, 1893177778},
{ ~N[2016-03-28 00:00:00], 2037.89, 2042.67, 2031.96, 2037.05, 1560605556},
{ ~N[2016-03-29 00:00:00], 2035.75, 2055.91, 2028.31, 2055.01, 2123516667},
{ ~N[2016-03-30 00:00:00], 2058.27, 2072.21, 2058.27, 2063.95, 1994616667},
{ ~N[2016-03-31 00:00:00], 2063.77, 2067.92, 2057.46, 2059.74, 2064044444},
{ ~N[2016-04-01 00:00:00], 2056.62, 2075.07, 2043.98, 2072.78, 2083327778},
{ ~N[2016-04-04 00:00:00], 2073.19, 2074.02, 2062.57, 2066.13, 1936505556},
{ ~N[2016-04-05 00:00:00], 2062.5, 2062.5, 2042.56, 2045.17, 2308288889},
{ ~N[2016-04-06 00:00:00], 2045.56, 2067.33, 2043.09, 2066.66, 2083777778},
{ ~N[2016-04-07 00:00:00], 2063.01, 2063.01, 2033.8, 2041.91, 2111805556},
{ ~N[2016-04-08 00:00:00], 2045.54, 2060.63, 2041.69, 2047.6, 1866405556},
{ ~N[2016-04-11 00:00:00], 2050.23, 2062.93, 2041.88, 2041.99, 1982133333},
{ ~N[2016-04-12 00:00:00], 2043.72, 2065.05, 2039.74, 2061.72, 2355411111},
{ ~N[2016-04-13 00:00:00], 2065.92, 2083.18, 2065.92, 2082.42, 2328794444},
{ ~N[2016-04-14 00:00:00], 2082.89, 2087.84, 2078.13, 2082.78, 2092150000},
{ ~N[2016-04-15 00:00:00], 2083.1, 2083.22, 2076.31, 2080.73, 2056361111},
{ ~N[2016-04-18 00:00:00], 2078.83, 2094.66, 2073.65, 2094.34, 1842711111},
{ ~N[2016-04-19 00:00:00], 2096.05, 2104.05, 2091.68, 2100.8, 2164905556},
{ ~N[2016-04-20 00:00:00], 2101.52, 2111.05, 2096.32, 2102.4, 2324933333},
{ ~N[2016-04-21 00:00:00], 2102.09, 2103.78, 2088.52, 2091.48, 2319605556},
{ ~N[2016-04-22 00:00:00], 2091.49, 2094.32, 2081.2, 2091.58, 2105877778},
{ ~N[2016-04-25 00:00:00], 2089.37, 2089.37, 2077.52, 2087.79, 1844300000},
{ ~N[2016-04-26 00:00:00], 2089.84, 2096.87, 2085.8, 2091.7, 1976216667},
{ ~N[2016-04-27 00:00:00], 2092.33, 2099.89, 2082.31, 2095.15, 2277838889},
{ ~N[2016-04-28 00:00:00], 2090.93, 2099.3, 2071.62, 2075.81, 2394355556},
{ ~N[2016-04-29 00:00:00], 2071.82, 2073.85, 2052.28, 2065.3, 2613733333},
{ ~N[2016-05-02 00:00:00], 2067.17, 2083.42, 2066.11, 2081.43, 2133950000},
{ ~N[2016-05-03 00:00:00], 2077.18, 2077.18, 2054.89, 2063.37, 2318550000},
{ ~N[2016-05-04 00:00:00], 2060.3, 2060.3, 2045.55, 2051.12, 2254755556},
{ ~N[2016-05-05 00:00:00], 2052.95, 2060.23, 2045.77, 2050.63, 2226961111},
{ ~N[2016-05-06 00:00:00], 2047.77, 2057.72, 2039.45, 2057.14, 2109083333},
{ ~N[2016-05-09 00:00:00], 2057.55, 2064.15, 2054.31, 2058.69, 2104788889},
{ ~N[2016-05-10 00:00:00], 2062.63, 2084.87, 2062.63, 2084.39, 2000111111},
{ ~N[2016-05-11 00:00:00], 2083.29, 2083.29, 2064.46, 2064.46, 2123322222},
{ ~N[2016-05-12 00:00:00], 2067.17, 2073.99, 2053.13, 2064.11, 2101327778},
{ ~N[2016-05-13 00:00:00], 2062.5, 2066.79, 2043.13, 2046.61, 1988822222},
{ ~N[2016-05-16 00:00:00], 2046.53, 2071.88, 2046.53, 2066.66, 1945200000},
{ ~N[2016-05-17 00:00:00], 2065.04, 2065.69, 2040.82, 2047.21, 2282755556},
{ ~N[2016-05-18 00:00:00], 2044.38, 2060.61, 2034.49, 2047.63, 2278511111},
{ ~N[2016-05-19 00:00:00], 2044.21, 2044.21, 2025.91, 2040.04, 2137094444},
{ ~N[2016-05-20 00:00:00], 2041.88, 2058.35, 2041.88, 2052.32, 1948694444},
{ ~N[2016-05-23 00:00:00], 2052.23, 2055.58, 2047.26, 2048.04, 1697488889},
{ ~N[2016-05-24 00:00:00], 2052.65, 2079.67, 2052.65, 2076.06, 2015188889},
{ ~N[2016-05-25 00:00:00], 2078.93, 2094.73, 2078.93, 2090.54, 2143977778},
{ ~N[2016-05-26 00:00:00], 2091.44, 2094.3, 2087.08, 2090.1, 1794994444}
]

dataset = Dataset.new( data, ["Date", "Open", "High", "Low", "Close", "Volume"])

alias Contex.OHLC.MA

opts = [
mapping: %{ datetime: "Date", open: "Open", high: "High", low: "Low", close: "Close"},
style: :candle,
title: "SPX",
zoom: 3,
bull_color: "00FF77",
bear_color: "FF3333",
shadow_color: "000000",
crisp_edges: true,
body_border: true,
timeframe: Contex.TimeScale.timeframe_d1(),
y_scale_window: true,
overlays: [
MA.new( period: 5, color: "0000AA", width: 2)
]
]

Contex.Plot.new( dataset, Contex.OHLC, 1200, 800, opts)
|> Contex.Plot.titles( "D1 Candlestick Chart", nil)
2 changes: 2 additions & 0 deletions lib/chart/gallery/ohlc_charts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ defmodule Contex.Gallery.OHLCCharts do
#{graph(title: "A simple tick OHLC chart",
file: "ohlc_tick.sample")}

#{graph(title: "D1 timeframe OHLC chart",
file: "ohlc_candle_d1.sample")}

"""
def plain(), do: 0
Expand Down
27 changes: 24 additions & 3 deletions lib/chart/lineplot.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,8 @@ defmodule Contex.LinePlot do
A column in the dataset can optionally be used to control the colours. See
`colours/2` and `set_colour_col_name/2`
"""

import Extructure
import Contex.SVG

alias __MODULE__
alias Contex.{Scale, ContinuousLinearScale, TimeScale}
alias Contex.CategoryColourScale
Expand Down Expand Up @@ -186,7 +185,11 @@ defmodule Contex.LinePlot do

@doc false
def to_svg(%LinePlot{} = plot, plot_options) do
plot = prepare_scales(plot)
plot =
plot
|> prepare_scales()
|> maybe_override_transforms()

x_scale = plot.x_scale
y_scale = plot.y_scale

Expand Down Expand Up @@ -216,6 +219,24 @@ defmodule Contex.LinePlot do
]
end

# Replaces original x & y transforms if so specified in options
@spec maybe_override_transforms(t()) :: t()
defp maybe_override_transforms(plot) do
[_x_transform, _y_transform] <~ plot.options

%{
plot
| transforms:
Map.merge(
plot.transforms,
%{
x: x_transform || plot.transforms.x,
y: y_transform || plot.transforms.y
}
)
}
end

defp get_x_axis(x_scale, plot) do
rotation =
case get_option(plot, :axis_label_rotation) do
Expand Down
12 changes: 12 additions & 0 deletions lib/chart/mapping.ex
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,18 @@ defmodule Contex.Mapping do
}
end

@doc """
Updates the dataset while ensuring sure the required columns are still present.
"""
@spec update_dataset!(t(), (Contex.Dataset.t() -> Contex.Dataset.t())) :: t()
def update_dataset!(mapping, updater) do
mapping = %{mapping | dataset: updater.(mapping.dataset)}

confirm_columns_in_dataset!(mapping.dataset, mapping.column_map)

mapping
end

@doc """
Given a plot that already has a mapping and a new map of elements to columns,
updates the mapping accordingly and returns the plot.
Expand Down
Loading
Loading