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

Ohlc scale #94

wants to merge 24 commits into from

Conversation

DaTrader
Copy link
Contributor

@DaTrader DaTrader commented Feb 4, 2024

Add fixed spacing and timeframes

@mindok
Copy link
Owner

mindok commented Feb 5, 2024

Hi @DaTrader - you've been busy! There's a bit to take in here.

First up, I really like the option to be able to explicitly set tick intervals. I think we can probably do better than directly interacting with the struct from Contex.OHLC as it feels like something useful for many situations. I'm thinking maybe adding a set_tick_interval to the Context.Scale protocol and implementing it across all scale types that implement that protocol. I'm happy to take this on if you like - let me know. We can still roll it into this PR.

The Contex.TimeScale.timeframe_xxx() functions look a bit messy to me. Maybe because I don't understand what they are doing. Are you happy to add a bit of documentation / a few notes about the purpose and how they are used in the scale calcs so we can possibly think about the best way to name them.

With the steps option - is this not redundant if the tick interval can be explicitly set? Or am I misunderstanding the interactions of timeframes & zoom levels?

@DaTrader
Copy link
Contributor Author

DaTrader commented Feb 5, 2024

Re set_tick_interval: Sure, go ahead.

Re Contex.TimeScale.timeframe_xxx(): Those are simply standard timeframes (tick intervals in your parlance), i.e. a subset of them. In charting tools, a timeframe is usually expressed by a single value, either in minutes or seconds, so if minutes, then m1 is 60, d1 is 1440, etc. I didn't do this because you already have your tuples for that so I simply named some of them. What I wasn't sure was where to put them but since so far those tuple definitions have been internal to the TimeScale module, I put them there. And yes, I could add some docs.

Re step: It's not redundant with the tick interval. Step is something that would otherwise be hard-coded into a trading/charting app, but in this case it's an additional parameter because everything needs to be aligned with the generalizations that you need for other types of charts. The logic is as follows:

  • tick_interval is the timeframe we're observing - the unit of each single "tick" (bar/candle) represents
  • interval_count is the amount of them within a particular time window (with the "width")
  • step is their frequency in the timescale because there's no room to display them all as the bars/candles can be very tiny and condensed (without any spacing - see zoom level 0), so this 'step' is inversely proportional to the total fixed space a bar/candle occupies (body width + spacing + borders), as the thinner the bar/candle the larger the step of which time instances to display. Thus, you can have your timeframe (tick_interval) set to d1 so each candle represents the OHLC of a day and then zoom in or zoom out which will have more or less candles shown at the same time respectively (according to the change in their total fixed width) and have it inversely reflected in the step parameter as we always need to keep the same (similar) density of and distance between the timescale tick grid and labels. To clarify, zoom is not in any way related to the timeframe, but only to the total fixed width of each of the candles. So, each timeframe can be zoomed at any of the supported zoom levels (from 0 - the most condensed one to 6 - the most widening one).

@mindok
Copy link
Owner

mindok commented Feb 7, 2024

Aha moment for me re: step... - the idea of tick_interval in Contex (and other generalised charting packages) is to determine how often to display a tick on the axis, and interval_count refers to the number of labels (approximately) that you want on the axis. It refers only to the scale & axis, and not to the data. I think this be a naming clash with the meaning of tick in financial charting.

What about if we had data_interval to indicate what each candle represents (this would be a time_frame_xxx). The tick_interval would then be calculated using the logic you currently have using data_interval, width and zoom, and step would go away. This maintains the existing meaning of tick_interval but gives you what you need.

With the timeframes, they are a bit specialised to financial charting. Are you likely to create a separate volume chart too? If so, I'm thinking we move the financial specific bits into a specialist scale that could be shared between them. If not, it's probably best to keep the timeframe functions in with the OHLC chart.

I don't have much time over the next couple of days, but happy to take on these, or happy for you to - let me know!

@DaTrader
Copy link
Contributor Author

DaTrader commented Feb 10, 2024

I haven't figured out how to use the tick_interval and interval_count in the way that you suggest (and without step) - to affect just the time scale and not the data. The labels used to map 1:1 to data ticks/bars or whatever you call them. So, a lower interval_count number simply meant equally less data units (candles) and labels.

This weekend I'm going to focus on completing the todos in the # todo: comments in ohlc.ex and then when you find the time, you can rename/refactor the fields and variables you mention across the modules that use them.

Volume is typically optional and when displayed it's an overlay chart limited in height relative to the bar/candlestick chart it is displayed in. Note that displaying the volume does not affect the value axis. The reason for this is the same as for why it is so common to make the timescale show only some of the labels having the aesthetics matter more than information - it is because all such charts are pointer sensitive. When hovering over candles with the mouse pointer, or over the candle close border as in MT4, a tooltip should be shown with the OHLC and volume values. This is something that needs to be done yet.

However, more important than volume are the so called indicator and oscillator charts, with the former being of more importance to me personally. Indicator charts are simply full chart overlays that share the same X and Y axes with the main (price action) chart. Oscillator charts are charts that share just the time scale and get optionally inserted together with their own value axis vertically between the price action chart and the time scale.

Last but not least is a free style (trend-)line and trendline based indicator drawing tool.

@DaTrader
Copy link
Contributor Author

DaTrader commented Feb 11, 2024 via email

@DaTrader
Copy link
Contributor Author

DaTrader commented Feb 25, 2024

Hey @mindok, can you please check my previous questions here from 2 weeks ago?

In addition to those, I now see that some charts are using this Mapping.update feature, implying the dataset in the plot.mapping is actually the point of reference and not the dataset in, say, the OHLC structure's dataset field. So, my question is now why do we have the latter in the first place (the redundancy) and can we only have the dataset only in the Mapping then (given that the whole Mapping structure is a part of the OHLC structure anyway)?

I am asking this because I'm now looking into adding the chart overlays (like a line chart over the candlestick chart). I'm thinking about adding the feature to the OHLC module - for there I already distinct between your stretched and mine fixed spacing. However, it then won't be reusable with charts other than OHLC but in absence of your assistance here, it's the only feasible way for me for now. Alternatively, you could help and try encapsulating the fixed spacing logic that I implemented within the OHLC module into a separate TimeScale-like module which would then hopefully be reusable with other types of charts like LinePlot.

What say you?

UPDATE:

I realized the OHLC redundant dataset can be removed in favor of keeping just the mapping's and just did so and pushed it.

Now, the question remaining is whether I go for implementing overlay charts as part of the OHLC chart only, or you can jump in and create a new fixed spacing TimeScale based on what I've so far implemented in the OHLC module, that all chart types can benefit from.

@mindok
Copy link
Owner

mindok commented Feb 26, 2024

Hi @DaTrader, re: the mapping / dataset - the mapping came in much later than the original dataset concept where column names were mapped to chart features via explicit functions. Mapping.update was an interim measure to allow deprecated functions to use the Mapping module, so Dataset was really the point of reference. However, thinking about it some more, it probably makes more sense to use your approach where Mapping is the point of reference internally. I'd be pretty certain that holding two copies of Dataset won't double the memory. From https://hexdocs.pm/elixir/lists-and-tuples.html:

Note, however, the elements themselves are not copied. When you update a tuple, all entries are shared between the old and the new tuple, except for the entry that has been replaced. This rule applies to most data structures in Elixir. This reduces the amount of memory allocation the language needs to perform and is only possible thanks to the immutable semantics of the language.

The external API for a plot is still better off looking like this IMO (i.e. mapping is an option for a chart of some data), particularly as mappings aren't required to be specified in certain circumstances:

dataset = Contex.Dataset.new(data, ["Category" | series_cols])

options = [
  mapping: %{category_col: "Category", value_cols: ["Series 1"]},
  type: :stacked,
  data_labels: true,
  orientation: :horizontal,
  colour_palette: ["1e293b"],
  series_columns: series_cols
]

Contex.Plot.new(dataset, Contex.BarChart, 500, 400, options)
  |> Contex.Plot.titles("", "")
  |> Contex.Plot.axis_labels("", "")
  |> Contex.Plot.plot_options(%{})

I'd suggest you do your overlay charts without waiting for me - I'm up to my neck in other stuff at the moment. If you add a couple of gallery charts to illustrate key points (e.g. how the time frames work etc), I will make sure they don't break during a refactor.

@DaTrader
Copy link
Contributor Author

@mindok Yeah, I am aware of the Erlang feature and wasn't really worried about the memory but about the inconsistency b/w the two dataset instances and the need to apply any update operation to both of them. I am saying this because I am trimming (updating) the dataset in the fixed spacing mode b/c there's no point of holding or displaying more candles than can fit in visually (as the initially supplied data array can be "infinite"). Also, this remedies the side effect of an extra candle being drawn outside of the data area.

@DaTrader
Copy link
Contributor Author

@mindok I've drafted the overlay implementation with Simple Moving Average as an example. What it takes now is for you to see how to optimize/provide more flexible access (if at all).

My concern is the way I am extracting the LinePlot SVG path markup in the Contex.OHLC.MA.render/2 and I believe it should be done in a more straight forward manner (optimized + less error prone), but for that I guess it takes modifying the Contex.Plot module which I believe is better if you do it.

Note that I no longer trim OHLC data to display it properly. Instead I use the domain time window with a modifiable start (domain_min) so the user can choose which part of the chart is shown. Besides, this is mandatory for proper computation of (some) indicators relying on past values - also the reason why SMA start is shifted to the right by the amount of period candles required to compute the average.

y_transform: render_config.y_transform
]

Plot.new(dataset, Contex.LinePlot, 100, 100, options)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah - I think we should be able to do better than this! I'll see what makes sense in terms of exposing parts of the line plot so you don't have to generate the whole thing and then pick bits out.

@mindok
Copy link
Owner

mindok commented Mar 11, 2024

Thanks @DaTrader - I've been contemplating what Contex 2.0 looks like (even though 1.0 hasn't be released!!). Moving to Phoenix function components for all aspects of rendering is looking pretty attractive, and the overlay that you have built here is a perfect example of why I'm considering this approach.

I added a comment to your code - you don't need to action - it's more of a reminder to myself. I think we can expose some of the internals of line plot to make it easier to reuse.

@DaTrader
Copy link
Contributor Author

DaTrader commented Mar 24, 2024

Moving to Phoenix function components for all aspects of rendering is looking pretty attractive, and the overlay that you have built here is a perfect example of why I'm considering this approach.

@mindok I fully agree. In order to be efficiently usable, it will require the ability to selectively update/render only parts of the chart, i.e. in case of OHLC the current (most recent bar) when connected to a live feed.

Btw, take a look here at the first glimpse of OHLC put to use: https://github.com/adrenaline-trading/adrenaline

It depends on AlpineJS, so make sure you go to adrenaline/apps/adrenaline_web/assets and run the npm install before starting Phoenix.

Keys:

  • left/right arrow to move chart left and right by a single candle
  • page up/page down to move chart by page
  • start/end to move to chart start/end
  • +, - to zoom in and out

@DaTrader
Copy link
Contributor Author

DaTrader commented Mar 24, 2024

@mindok Btw, at some point you'll need to make the X and Y axes non-transparent and above the data chart to prevent the following from happening:

image

PS. I can prevent this particular one by taking into account the overlay (the moving average in this example) values when the Y axis is computed.

@mindok
Copy link
Owner

mindok commented Mar 25, 2024

Nice job @DaTrader! The overlay has come out well. It's really useful to see a full example and gives a good target to test against with some of the rearranging proposed above.

WRT Phoenix Components - maybe optionally handling the stream approach would help with pushing / popping individual data points from what's rendered. Working around the constraints will be a challenge though.

DaTrader added 5 commits May 5, 2024 18:33
optimize rendering loop;
make optional domain (visible candles) window based y scale computation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants