Accept CamelCase feature names in mix backtest

mix backtest --feature NaiveGradient now resolves to the same function
as --feature naive_gradient. Names are normalized via Macro.underscore,
so any casing works. Also prints the available functions when an
unknown feature is requested so typos don't produce an opaque
UndefinedFunctionError deep in the Enum.map stack.
This commit is contained in:
Graham McIntire 2026-04-09 16:15:01 -05:00
parent ab04cb9168
commit 7630934bcb

View file

@ -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