cyberkyne's picture
Upload 22 files
1a56c89 verified
"""
Optimizer.jl — Walk-forward optimization engine.
No includes. BacktestConfig/run_backtest/BacktestResult received via QuantEngine.
"""
module Optimizer
using Statistics, Random
export walk_forward_optimize, OptimResult
mutable struct OptimResult
strategy_name::String; symbol::String; timeframe::String
optimal_params::Dict{String,Float64}
oos_sharpe_mean::Float64; oos_sharpe_std::Float64
oos_win_rate::Float64; oos_max_dd::Float64; oos_pf_mean::Float64
oos_trades::Int; wf_efficiency::Float64; robustness::Float64
is_viable::Bool; reasons::Vector{String}; oos_sharpes::Vector{Float64}
end
OptimResult(n,s,t) = OptimResult(n,s,t,Dict{String,Float64}(),
0.0,0.0,0.0,0.0,0.0,0,0.0,0.0,false,String[],Float64[])
function walk_forward_optimize(
signal_fn::Function,
param_grid::Dict{String,Vector{Float64}},
open_p::Vector{Float64}, high::Vector{Float64},
low::Vector{Float64}, close::Vector{Float64},
volume::Vector{Float64}, timeframe::String,
strategy_name::String, symbol::String;
run_bt_fn::Function, # run_backtest injected from QuantEngine
bt_cfg_fn::Function, # BacktestConfig() constructor injected
n_windows::Int=5, is_ratio::Float64=0.70,
min_trades::Int=30, min_sharpe::Float64=0.5,
max_combos::Int=300,
)::OptimResult
result = OptimResult(strategy_name, symbol, timeframe)
n = length(close)
n < 200 && (push!(result.reasons,"Need ≥200 bars, got $n"); return result)
isempty(param_grid) && (param_grid = Dict{String,Vector{Float64}}())
cfg = bt_cfg_fn()
combos = _build_combos(param_grid, max_combos)
windows = _windows(n, n_windows)
isempty(windows) && (push!(result.reasons,"No WF windows"); return result)
win_params=Vector{Dict{String,Float64}}()
is_sharpes=Float64[]; oos_sharpes=Float64[]
oos_results=[]
for (is_s,is_e,oos_s,oos_e) in windows
best_p=nothing; best_sh=-Inf
for p in combos
r = _run(signal_fn,run_bt_fn,cfg,
open_p[is_s:is_e],high[is_s:is_e],
low[is_s:is_e],close[is_s:is_e],
volume[is_s:is_e],p,timeframe)
r.is_valid && r.n_trades>=min_trades && r.sharpe>best_sh && (best_sh=r.sharpe; best_p=p)
end
best_p===nothing && continue
push!(win_params,best_p); push!(is_sharpes,best_sh)
oos_r = _run(signal_fn,run_bt_fn,cfg,
open_p[oos_s:oos_e],high[oos_s:oos_e],
low[oos_s:oos_e],close[oos_s:oos_e],
volume[oos_s:oos_e],best_p,timeframe)
push!(oos_results,oos_r); push!(oos_sharpes,oos_r.sharpe)
end
isempty(oos_results) && (push!(result.reasons,"No valid WF windows"); return result)
result.oos_sharpes = oos_sharpes
valid = filter(r->r.is_valid && r.n_trades>=min_trades, oos_results)
if !isempty(valid)
sh=[r.sharpe for r in valid]
result.oos_sharpe_mean=mean(sh); result.oos_sharpe_std=std(sh)
result.oos_win_rate=mean([r.win_rate for r in valid])
result.oos_max_dd=mean([r.max_dd for r in valid])
pfs=filter(x->x<100,[r.profit_factor for r in valid])
result.oos_pf_mean=isempty(pfs) ? 0.0 : mean(pfs)
result.oos_trades=sum(r.n_trades for r in valid)
end
if !isempty(is_sharpes) && !isempty(oos_sharpes)
mis=mean(is_sharpes); mos=mean(oos_sharpes)
result.wf_efficiency = mis>0 ? mos/mis : 0.0
end
result.optimal_params = _vote(win_params, oos_sharpes)
result.robustness = _robustness(result, min_trades)
result.is_viable, result.reasons = _viability(result, min_trades, min_sharpe)
return result
end
function _run(sig_fn,run_bt,cfg,o,h,l,c,v,params,tf)
try
sigs = sig_fn(o,h,l,c,v,params)
return run_bt(o,h,l,c,v,sigs,tf,cfg)
catch e1
# Strategy failed — run with flat signals to get a valid struct back
try
r = run_bt(o,h,l,c,v,zeros(Int,length(c)),tf,cfg)
r.is_valid = false
r.error_msg = string(e1)
return r
catch e2
# Even the fallback failed — return manually constructed invalid result
return run_bt(o[1:2],h[1:2],l[1:2],c[1:2],v[1:2],
zeros(Int,2),tf,cfg) # will hit n<50 guard → is_valid=false
end
end
end
function _build_combos(grid::Dict{String,Vector{Float64}}, max_c::Int)::Vector{Dict{String,Float64}}
isempty(grid) && return [Dict{String,Float64}()]
ks=collect(keys(grid)); vs=[grid[k] for k in ks]
all_c=Dict{String,Float64}[]
function recurse(i,current)
if i>length(ks); push!(all_c,copy(current)); return; end
for v in vs[i]; current[ks[i]]=v; recurse(i+1,current); end
end
recurse(1,Dict{String,Float64}())
length(all_c)>max_c && (all_c=all_c[randperm(length(all_c))[1:max_c]])
return all_c
end
function _windows(n::Int,nw::Int)::Vector{Tuple{Int,Int,Int,Int}}
osz=max(50,n÷(nw*2)); wins=Tuple{Int,Int,Int,Int}[]
for i in 0:(nw-1)
oe=n-i*osz; os=oe-osz+1; ie=os-1
ie-1<100||oe-os<50 && continue
push!(wins,(1,ie,os,oe))
end
return reverse(wins)
end
function _vote(pl::Vector{Dict{String,Float64}}, oos::Vector{Float64})::Dict{String,Float64}
isempty(pl) && return Dict{String,Float64}()
length(pl)==1 && return pl[1]
w=max.(0.0,oos[1:length(pl)]); tw=sum(w)
w = tw>0 ? w./tw : fill(1.0/length(pl),length(pl))
ks=collect(keys(pl[1])); result=Dict{String,Float64}()
for k in ks
vals=[p[k] for p in pl if haskey(p,k)]
wi=w[1:length(vals)]
si=sortperm(vals); cv=cumsum(wi[si])
mi=findfirst(x->x>=0.5,cv)
result[k]=vals[si[mi!==nothing ? mi : end]]
end
return result
end
function _robustness(r::OptimResult, mt::Int)::Float64
s=clamp(r.wf_efficiency,0.0,1.0)*40.0
r.oos_sharpe_mean>0 && (s+=clamp(1.0-r.oos_sharpe_std/(r.oos_sharpe_mean+1e-9),0.0,1.0)*30.0)
s+=clamp(r.oos_trades/max(1,mt*10),0.0,1.0)*20.0
r.oos_pf_mean>1 && (s+=clamp((r.oos_pf_mean-1)/2,0.0,1.0)*10.0)
return round(s;digits=1)
end
function _viability(r::OptimResult,mt::Int,ms::Float64)::Tuple{Bool,Vector{String}}
reasons=String[]
r.oos_sharpe_mean<ms && push!(reasons,"OOS Sharpe $(round(r.oos_sharpe_mean;digits=2)) < $ms")
r.oos_trades<mt && push!(reasons,"Too few OOS trades: $(r.oos_trades) < $mt")
r.oos_max_dd>30.0 && push!(reasons,"High avg DD: $(round(r.oos_max_dd;digits=1))%")
r.wf_efficiency<0.3 && push!(reasons,"Low WFE: $(round(r.wf_efficiency;digits=2))")
r.oos_pf_mean<1.1 && push!(reasons,"PF $(round(r.oos_pf_mean;digits=2)) < 1.1")
viable=isempty(reasons)
viable && push!(reasons,"✅ Sharpe=$(round(r.oos_sharpe_mean;digits=2)) DD=$(round(r.oos_max_dd;digits=1))% WFE=$(round(r.wf_efficiency;digits=2)) Score=$(r.robustness)/100")
return viable,reasons
end
end # module Optimizer