""" 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_mean30.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