Creating User Defined Layers

Here is an example of how to add a new user defined layer for calculating simple moving averages (SMA).

Define The Layer Struct

mutable struct LayerSMA <: LayersMovingAverage
    ma_values::Dict{Int64, Vector{Float64}} 
end

First, define a mutable struct for the layer. The type of the added layer should be a subtype of some abstryact subtype of AssetLayer - which in this this example menas a subtype of LayersMovingAverage which in its turn is a subtype of AssetLayer.

Further, the struct should contain all necessary fields for the layer to store its state. In the case of a non-repainting layer (like SMA) this can often be in the type of Vectors of values, while in the case of a repainting layer it often needs to be Vectors of Vectors of values. Here, the layer supports many window sizes simultaneously and therefore it needs to be a Dict of Vectors.

Define The Repainting Trait

function LayerRepaintingTrait(::Type{<:LayerSMA})
    return NonRepainting()
end

Next, define a function to specify the repainting trait. SMA is a non-repainting indicator, so return NonRepainting():

Define Default Parameters

Then define a function to return the default parameters for the layer:

function default_parameters(::Type{LayerSMA})
    return Parameters(LayerSMA, Dict{Symbol, Any}(
        :win_sizes => [45, 60], 
        :price => DefaultPrice()))
end

Implement The Layer Logic

The main logic goes in _produce_layer. Get the parameters, calculate the SMA values, and return the layer struct. The variable to_keep representing which values to keep is returned along the layer struct.

Various types of DataViews are typically used as input. For non-repainting layers it is common to use NonRepainting DataViews.

function _produce_layer(::Type{LayerSMA}, asset::Asset, settings::Settings, settings_used::Settings; common_input=nothing)

    # input scalars
    win_sizes = get_parameter(settings, LayerSMA, :win_sizes) 
    price_field = get_parameter(settings, LayerSMA, :price)
    win_max = maximum(win_sizes)

    # input vectors
    dvts = DVNonRepaintingTimeSeriesBack(asset, price_field, win_max-1) 

    sma_values = Dict{Int64, Vector}()

    for win_size in win_sizes
        sma = zeros(length(dvts))
        window = CircularDeque{Float64}(win_size)

        # calculate SMA
        ...

        sma_values[win_size] = sma
    end

    # to keep
    to_keep = trues(length(asset))

    return LayerSMA(sma_values), to_keep
end

Adding Layer Specific Plotting

function plot_graph(asset::Asset{T,N}, ind::Integer; settings_used=Settings()) where T where N <: LayerSMA
    # Initialize an array to hold plots, starting with any plots from the parent asset.
    plots = [plot_graph(asset.parent_asset, ind; settings_used=settings_used)]

    # Retrieve the date indexes for plotting, ensuring alignment with the asset's timeline.
    x = dates_index_for_plot(asset, settings_used, ind)

    # Extract the window sizes from the layer's settings..
    win_sizes = settings_used[LayerSMA][:win_sizes]

    # Iterate through each window size to prepare and plot SMA data.
    for win_size in win_sizes
        # Extract SMA values for the current window size using the asset and sample index.
        y = DVLayerDictKey(asset, :ma_values, win_size)[ind]

        # Plotting is performed with PlotlyLight.
        p = PlotlyLight.Plot(; x=x, y=y, name="win size: "*string(win_size))

        # Add the current plot to the collection of plots to be displayed.
        push!(plots, p)
    end

    # Combine all individual plots into a single plot object.
    # The "first.(getfield.(plots, :data)) extracts the relevant data from
    # each plotting object and is a PlotlyLight specific technicality.
    # Just accept ;)
    return PlotlyLight.Plot(first.(getfield.(plots, :data)), layout_for_graph())
end