dysonance / Strategems.jl

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

Parameter Set Constraints #8

Open dysonance opened 6 years ago

dysonance commented 6 years ago

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.

femtotrader commented 6 years ago

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

femtotrader commented 6 years ago

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

femtotrader commented 6 years ago

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