dysonance / Strategems.jl

Quantitative systematic trading strategy development and backtesting in Julia
Other
163 stars 39 forks source link

ParameterSet, parameters as named tuples and constraints #29

Open femtotrader opened 1 year ago

femtotrader commented 1 year ago

Hello,

ParameterSet currently doesn't support constraints among parameters.

Here is a short implementation

import Base: show

const TABWIDTH = 4
const TAB = ' ' ^ TABWIDTH

mutable struct ParameterSet
    arg_names::Vector{Symbol}
    arg_defaults::Vector
    arg_ranges::Vector
    arg_types::Vector{<:Type}
    n_args::Int
    constraints::Vector
    #TODO: refactor out the arg_ prefix (its redundant if they all start with it)
    function ParameterSet(arg_names::Vector{Symbol},
                          arg_defaults::Vector,
                          arg_ranges::Vector=[x:x for x in arg_defaults];
                          constraints::Vector = [p -> true])
        @assert length(arg_names) == length(arg_defaults) == length(arg_ranges)
        @assert eltype.(arg_defaults) == eltype.(arg_ranges)
        arg_types::Vector{<:Type} = eltype.(arg_defaults)
        return new(arg_names, arg_defaults, arg_ranges, arg_types, length(arg_names), constraints)
    end
end

function generate_nt_combinations(ps::ParameterSet)
    itr = Iterators.product(ps.arg_ranges...)
    itr = Iterators.map(t -> (; zip(ps.arg_names, t)...), itr)
    for constraint in ps.constraints
        itr = Iterators.filter(constraint, itr)
    end
    collect(itr)
end

Usage:

arg_names     = [:fastlimit, :slowlimit, :x]
arg_defaults  = [0.5, 0.05, 0.01]
arg_ranges    = [0.01:0.01:1.00, 0.01:0.01:1.00, 0.01:0.01:1.00]
constraints   = [p -> p.fastlimit < p.slowlimit]
paramset      = ParameterSet(arg_names, arg_defaults, arg_ranges, constraints=constraints)
params = generate_nt_combinations(paramset)
params

returns

495000-element Vector{NamedTuple{(:fastlimit, :slowlimit, :x), Tuple{Float64, Float64, Float64}}}:
 (fastlimit = 0.01, slowlimit = 0.02, x = 0.01)
 (fastlimit = 0.01, slowlimit = 0.03, x = 0.01)
 (fastlimit = 0.02, slowlimit = 0.03, x = 0.01)
 (fastlimit = 0.01, slowlimit = 0.04, x = 0.01)
 (fastlimit = 0.02, slowlimit = 0.04, x = 0.01)
 ...
 (fastlimit = 0.96, slowlimit = 1.0, x = 1.0)
 (fastlimit = 0.97, slowlimit = 1.0, x = 1.0)
 (fastlimit = 0.98, slowlimit = 1.0, x = 1.0)
 (fastlimit = 0.99, slowlimit = 1.0, x = 1.0)

which can easily be transformed as DataFrame:

using DataFrame
DataFrame(params)

which returns

495000×3 DataFrame
    Row │ fastlimit  slowlimit  x
        │ Float64    Float64    Float64
────────┼───────────────────────────────
      1 │      0.01       0.02     0.01
      2 │      0.01       0.03     0.01
      3 │      0.02       0.03     0.01
      4 │      0.01       0.04     0.01
      5 │      0.02       0.04     0.01
      6 │      0.03       0.04     0.01
      7 │      0.01       0.05     0.01
      8 │      0.02       0.05     0.01
      9 │      0.03       0.05     0.01
     10 │      0.04       0.05     0.01
     11 │      0.01       0.06     0.01
   ⋮    │     ⋮          ⋮         ⋮
 494991 │      0.9        1.0      1.0
 494992 │      0.91       1.0      1.0
 494993 │      0.92       1.0      1.0
 494994 │      0.93       1.0      1.0
 494995 │      0.94       1.0      1.0
 494996 │      0.95       1.0      1.0
 494997 │      0.96       1.0      1.0
 494998 │      0.97       1.0      1.0
 494999 │      0.98       1.0      1.0
 495000 │      0.99       1.0      1.0
                     494979 rows omitted

Maybe it can help.

Kind regards

PS: see also #8

dysonance commented 1 year ago

Hey @femtotrader I think this kind of functionality does offer a lot of utility. Definitely appreciate you prototyping out an implementation. Would you mind opening a pull request integrating your proposed implementation into the package so that we can review it more holistically with test cases and examples and such?