Spaces:
Sleeping
Sleeping
| """ | |
| 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 | |