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