Seeking API Design Advice for Setting Defaults

I have a constructor for a mutable Chart struct that is used like this.

using OnlineTechnicalIndicators
using TechnicalIndicatorCharts

golden_cross_chart = Chart(
    "AAPL", Week(1);
    indicators = [
        SMA{Float64}(;period=50),         # Setup indicators
        SMA{Float64}(;period=200)
    ],
    visuals = [
        Dict(
            :label_name => "SMA 50",      # Describe how to draw indicators
            :line_color => "#E072A4",
            :line_width => 2
        ),
        Dict(
            :label_name => "SMA 200",
            :line_color => "#3D3B8E",
            :line_width => 5
        )
    ]
)

There’s a 1-to-1 correspondence between the values in indicators and the values in visuals. This is for my unreleased library TechnicalIndicatorCharts.jl.

What if I don’t care about colors?

If I don’t care how it looks, and I just want to use the defaults, this is what I currently have to do.

golden_cross_chart = Chart(
    "AAPL", Week(1);
    indicators = [
        SMA{Float64}(;period=50),         # Setup indicators
        SMA{Float64}(;period=200)
    ],
    visuals = [
        Dict(),                           # Not the prettiest, but tolerable.
        Dict()
    ]
)

It can get worse.

That might not be so bad, but what if I added Bollinger Bands which is drawn with 3 lines instead of just one like SMA.

golden_cross_chart = Chart(
    "AAPL", Week(1);
    indicators = [
        SMA{Float64}(;period=50),         # Setup indicators
        SMA{Float64}(;period=200),
        BB{Float64}(;period=20)
    ],
    visuals = [
        Dict(),                           # Not the prettiest
        Dict(),
        Dict(
            :upper => Dict(),             # Now it's getting ugly.
            :central => Dict(),
            :lower => Dict()              # All this typing just for defaults?!
        )
    ]
)

How should I do this?

I want to be able to declare defaults in the visuals vector with way less typing. How could this be accomplished?

1 Like

just a side note: in the post or in the package readme, specify you are dealing with financial technical indicators, that’s not super obvious from people not in the domain :wink:

2 Likes

I’m kinda curious what the non-financial kind of technical indicators might be. (The financial kind are the only ones I know.) To provide a hint to the reader, I added the finance tag to this thread.

In other news, I got a little bit of advice from someone over on Zulip. I was kinda losing hope on getting a response on this, but he had a nice idea that I’m working on implementing today.

Perhaps Im wrong, but I think you could just use (maybe typed) global variables in the module where you define chart and have keywords in the inner constructor.

module ChartVisuals
    
    bollinger_default = Dict(1 => "red")
    
    struct Chart{S,T}
        symbol::S
        visuals::T
        function Chart(s::S; visuals::T = bollinger_default) where {S,T}
            new{S,T}(s,visuals)
        end
    end

end

You can even update the global values in the module just like any other variable ChartVisuals.bollinger_default = Dict_of_new_colors, have a different global, ChartVisuals.sma_default.

Sometimes the ā€œdont use globalsā€ thinking gets a little too dogmatic. I think they have a nice place outside ā€œhot codeā€ like for package related preferences. Not sure if this is exactly what you want.

1 Like

I have to be honest. My Julia skill level is currently at the point where I don’t really know how to use where. Maybe I’ll learn today to fully understand your reply, but my understanding is not there yet.

Also, I need Chart to potentially contain multiple indicators so that they can be drawn together. It seems like you restructured Chart to only have one. That’s not enough for me.

Regarding globals, I’m not opposed to them if they’re essentially constants.

I know it looks freaky, but this pattern brings the types of the variables into scope so that S and T can be used at compile time to build the new type. Basically, if I need to use type parameters as variables, for constructing new objects, I use ā€œwhereā€ even if Im not enforcing fancy dispatch patterns (which is how where is usually discussed in the docs).

I didn’t want to learn it either, but I found ā€˜where’ to be super important, and found that it was neccesary in many cases to construct fully specified, fast, concrete types. You don’t need much more than the pattern you see above.

EDIT: yes I dropped the indicator field, I was just trying to concisely express the pattern sorry!

1 Like