Spaces:
Sleeping
Sleeping
File size: 7,014 Bytes
094a5f6 1a56c89 094a5f6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | """
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
|