dysonance/Strategems.jl

Parameter Set Constraints

Opened this issue · 3 comments

When defining a ParameterSet object with ranges corresponding to each of its variables (i.e. the arg_ranges member), currently there isn't any way to constrain the parameter space using the relationships between the variables.

There should be logic to support these kinds of inter-variable relationship constraints. For example, take a simple strategy using two moving averages, one with a short lookback period and one with a long lookback period. Suppose our rule is to go long when the short MA crosses over the long MA, and we want to simulate this strategy over a bunch of pairs of lookback windows. There should be logic to constrain the simulation such that the short MA's lookback must be less than the long MA's lookback.

What your are looking for looks like a CSP solver

With Python, there is

I don't know if there is project like this written in Julia.

I also wonder if https://github.com/JuliaOpt/JuMP.jl have such feature (or more generally https://github.com/JuliaOpt )

See http://nbviewer.jupyter.org/github/JuliaOpt/juliaopt-notebooks/tree/master/notebooks/ for some sample notebooks

python-constraint is "only" 1500 lines of code

A simpler approach with Channel and for loops

with Tuple for parameters

parameter_generator() = Channel(ctype=Tuple{Int,Int}) do c
    for short in 1:10
        for long in 1:10
            if short < long
                push!(c, (short, long))
            end
        end
    end
end

for (i, param) in enumerate(parameter_generator())
    short, long = param
    println("i=$i short=$short long=$long")
end

or with Dict for parameters

parameter_generator() = Channel(ctype=Dict{String,Int}) do c
    for short in 1:10
        for long in 1:10
            if short < long
                push!(c, Dict("short"=>short, "long"=>long))
            end
        end
    end
end

for (i, param) in enumerate(parameter_generator())
    short = param["short"]
    long = param["long"]    
    println("i=$i short=$short long=$long")
end

it displays:

i=1 short=1 long=2
i=2 short=1 long=3
i=3 short=1 long=4
i=4 short=1 long=5
i=5 short=1 long=6
i=6 short=1 long=7
i=7 short=1 long=8
i=8 short=1 long=9
i=9 short=1 long=10
i=10 short=2 long=3
i=11 short=2 long=4
i=12 short=2 long=5
i=13 short=2 long=6
i=14 short=2 long=7
i=15 short=2 long=8
i=16 short=2 long=9
i=17 short=2 long=10
i=18 short=3 long=4
i=19 short=3 long=5
i=20 short=3 long=6
i=21 short=3 long=7
i=22 short=3 long=8
i=23 short=3 long=9
i=24 short=3 long=10
i=25 short=4 long=5
i=26 short=4 long=6
i=27 short=4 long=7
i=28 short=4 long=8
i=29 short=4 long=9
i=30 short=4 long=10
i=31 short=5 long=6
i=32 short=5 long=7
i=33 short=5 long=8
i=34 short=5 long=9
i=35 short=5 long=10
i=36 short=6 long=7
i=37 short=6 long=8
i=38 short=6 long=9
i=39 short=6 long=10
i=40 short=7 long=8
i=41 short=7 long=9
i=42 short=7 long=10
i=43 short=8 long=9
i=44 short=8 long=10
i=45 short=9 long=10

Using NamedTuples could also be considered

Code updated with NamedTuples (from https://github.com/JuliaData/NamedTuples.jl )

mutable struct ParameterSet
    arg_names::Vector{Symbol}
    arg_defaults::Vector
    arg_ranges::Vector
    arg_types::Vector{<:Type}
    n_args::Int
    constraints::Vector{Function}
    function ParameterSet(arg_names::Vector{Symbol},
                          arg_defaults::Vector,
                          arg_ranges::Vector=[x:x for x in arg_defaults],
                          constraints=Vector{Function}())
        @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

import Base: length
length(ps::ParameterSet) = mapreduce(length, *, 1, ps.arg_ranges)  # inspired by IterTools.jl length(p::Product)

abstract type ParameterIteration end

struct CartesianProductIteration <: ParameterIteration
end


using IterTools
using NamedTuples: make_tuple

function get_iterator(ps::ParameterSet, method::CartesianProductIteration)
    itr = IterTools.product(ps.arg_ranges...)
    itr = map(t->make_tuple(ps.arg_names)(t...), itr)
    #itr = map(t->NamedTuple{Tuple(ps.arg_names)}(t...), itr)  # see https://github.com/JuliaData/NamedTuples.jl/issues/53
    for constraint in ps.constraints
        itr = Iterators.filter(constraint, itr)
    end
    itr
end




using Base.Test

# write your own tests here
@testset "Main tests" begin
    arg_names    = [:real1, :real2]
    arg_defaults = [0.5, 0.05]
    arg_ranges   = [0.01:0.01:0.99, 0.01:0.01:0.99]
    #constraints  = [
    #    p -> p[1] + p[2] < 1.0,
    #    p -> p[1] + p[2] > 0.5
    #]
    constraints  = [
        p -> p.real1 + p.real2 < 1.0,
        p -> p.real1 + p.real2 > 0.5
    ]
    
    ps           = ParameterSet(arg_names, arg_defaults, arg_ranges, constraints)

    @test length(ps) == 9801  # 99*99

    itr_method   = CartesianProductIteration()
    #itr_method   = RandomIteration()

    itr          = get_iterator(ps, itr_method)
    
    #for p in itr
    #    println(p)
    #end

    v = collect(itr)
    #println(v)
    @test length(v) == 3626
end

@testset "Other test" begin
    arg_names    = [:short, :long]
    arg_defaults = [1, 1]
    arg_ranges   = [1:10, 1:10]
    #constraints  = [
    #    p -> p[1] < p[2]
    #]
    constraints  = [
        p -> p.short < p.long
    ]
    ps           = ParameterSet(arg_names, arg_defaults, arg_ranges, constraints)

    @test length(ps) == 100  # 10*10

    itr_method   = CartesianProductIteration()
    #itr_method   = RandomIteration()

    itr          = get_iterator(ps, itr_method)

    v = collect(itr)

    @test length(v) == 45
end