diff --git a/lib/mix/tasks/backtest.ex b/lib/mix/tasks/backtest.ex index 0ff2efae..f1b1e9e4 100644 --- a/lib/mix/tasks/backtest.ex +++ b/lib/mix/tasks/backtest.ex @@ -7,14 +7,18 @@ defmodule Mix.Tasks.Backtest do ## Usage - mix backtest --feature Microwaveprop.Backtest.Features.naive_gradient mix backtest --feature naive_gradient + mix backtest --feature NaiveGradient # CamelCase also works + mix backtest --feature Microwaveprop.Backtest.Features.naive_gradient mix backtest --feature naive_gradient --sample 1000 --out priv/backtest_reports/naive.md ## Options * `--feature` (required) — fully-qualified `Module.function` or a - short name that lives on `Microwaveprop.Backtest.Features`. + short name that lives on `Microwaveprop.Backtest.Features`. Names + are normalized via `Macro.underscore/1`, so `NaiveGradient`, + `naive_gradient`, and `naiveGradient` all resolve to the same + function. * `--sample` — max number of QSOs to evaluate (default: 5000). * `--baseline` — random-baseline sample size (default: same as `--sample`). * `--out` — optional file path to write the report to in addition @@ -66,16 +70,36 @@ defmodule Mix.Tasks.Backtest do defp resolve_feature(spec) do case String.split(spec, ".") do [name] -> - fun = String.to_atom(name) + fun = resolve_function_atom!(Microwaveprop.Backtest.Features, name) feature_fun = &apply(Microwaveprop.Backtest.Features, fun, [&1, &2, &3]) - {feature_fun, "Microwaveprop.Backtest.Features.#{name}"} + {feature_fun, "Microwaveprop.Backtest.Features.#{fun}"} parts -> {fun_name, mod_parts} = List.pop_at(parts, -1) module = Module.concat(mod_parts) - fun = String.to_atom(fun_name) + fun = resolve_function_atom!(module, fun_name) feature_fun = &apply(module, fun, [&1, &2, &3]) - {feature_fun, Enum.join(parts, ".")} + {feature_fun, "#{inspect(module)}.#{fun}"} + end + end + + # Accept both `naive_gradient` and `NaiveGradient` and anything in between. + defp resolve_function_atom!(module, name) do + Code.ensure_loaded!(module) + normalized = Macro.underscore(name) + fun = String.to_atom(normalized) + + if function_exported?(module, fun, 3) do + fun + else + exported = + module.__info__(:functions) + |> Enum.filter(fn {_f, arity} -> arity == 3 end) + |> Enum.map_join(", ", fn {f, _} -> to_string(f) end) + + Mix.raise( + "Feature #{inspect(module)}.#{normalized}/3 is not defined.\nAvailable 3-arity functions: #{exported}" + ) end end end