
Evolutionary Metric Ranking
- 111 installs
- 62 repo stars
- Updated August 3, 2026
- terrylica/cc-skills
Use evolutionary-metric-ranking for development tasks
About
evolutionary-metric-ranking: A skill for development. This provides functionality for development workflows.
- evolutionary-metric-ranking
Evolutionary Metric Ranking by the numbers
- 111 all-time installs (skills.sh)
- Ranked #2,929 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/terrylica/cc-skills --skill evolutionary-metric-rankingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 111 |
|---|---|
| repo stars | ★ 62 |
| Last updated | August 3, 2026 |
| Repository | terrylica/cc-skills ↗ |
What it does
Use evolutionary-metric-ranking for development tasks
Files
Evolutionary Metric Ranking
Methodology for systematically zooming into high-quality configurations across multiple evaluation metrics using per-metric percentile cutoffs, intersection-based filtering, and evolutionary optimization. Domain-agnostic principles with quantitative trading case studies.
Companion skills: rangebar-eval-metrics (metric definitions) | adaptive-wfo-epoch (WFO integration) | backtesting-py-oracle (SQL validation)
---
Self-Evolving Skill: This skill improves through use. If instructions are wrong, parameters drifted, or a workaround was needed — fix this file immediately, don't defer. Only update for real, reproducible issues.
When to Use This Skill
Use this skill when:
- Ranking and filtering configs/strategies/models across multiple quality metrics
- Searching for optimal per-metric thresholds that select the best subset
- Identifying which metrics are binding constraints vs inert dimensions
- Running multi-objective optimization (Optuna TPE / NSGA-II) over filter parameters
- Performing forensic analysis on optimization results (universal champions, feature themes)
- Designing a metric registry for pluggable evaluation systems
---
Core Principles
P1 - Percentile Ranks, Not Raw Values
Raw metric values live on incompatible scales (Kelly in [-1,1], trade count in [50, 5000], Omega in [0.8, 2.0]). Percentile ranking normalizes every metric to [0, 100], making cross-metric comparison meaningful.
Rule: scipy.stats.rankdata(method='average') scaled to [0, 100]
None/NaN/Inf -> percentile 0 (worst)
"Lower is better" metrics -> negate before ranking (100 = best)Why average ties: Tied values receive the mean of the ranks they would span. This prevents artificial discrimination between genuinely identical values.
P2 - Independent Per-Metric Cutoffs
Each metric gets its own independently-tunable cutoff. cutoff=20 means "only configs in the top 20% survive this filter." This creates a 12-dimensional (or N-dimensional) search space where each axis controls one quality dimension.
cutoff=100 -> no filter (everything passes)
cutoff=50 -> top 50% survives
cutoff=10 -> top 10% survives (stringent)
cutoff=0 -> nothing passesWhy independent, not uniform: Different metrics have different discrimination power. Uniform tightening (all metrics at the same cutoff) wastes filtering budget on inert dimensions while under-filtering on binding constraints.
P3 - Intersection = Multi-Metric Excellence
A config survives the final filter only if it passes ALL per-metric cutoffs simultaneously. This intersection logic ensures no single-metric champion sneaks through with terrible performance elsewhere.
survivors = metric_1_pass AND metric_2_pass AND ... AND metric_N_passWhy intersection, not scoring: Weighted-sum scoring hides metric failures. A config with 99th percentile Sharpe but 1st percentile regularity would score well in a weighted sum but is clearly deficient. Intersection enforces minimum quality across every dimension.
P4 - Start Wide Open, Tighten Evolutionarily
All cutoffs default to 100% (no filter). The optimizer progressively tightens cutoffs to find the combination that best satisfies the chosen objective. This is the opposite of starting strict and relaxing.
Initial state: All cutoffs = 100 (1008 configs survive)
After search: Each cutoff independently tuned (11 configs survive)Why start wide: Starting strict risks missing the global optimum by immediately excluding configs that would survive under a different cutoff combination. Wide-to-narrow exploration is characteristic of global optimization.
P5 - Multiple Objectives Reveal Different Truths
No single objective function captures "quality." Run multiple objectives and compare survivor sets. Configs that survive all objectives are the most robust.
| Objective | Asks | Reveals |
|---|---|---|
| max_survivors_min_cutoff | Most configs at tightest cutoffs? | Efficient frontier of quantity vs stringency |
| quality_at_target_n | Best quality in top N? | Optimal cutoffs for a target portfolio size |
| tightest_nonempty | Absolute tightest with >= 1 survivor? | Universal champion (sole survivor) |
| pareto_efficiency | Survivors vs tightness trade-off? | Full Pareto front (NSGA-II) |
| diversity_reward | Are cutoffs non-redundant? | Which metrics provide independent information |
Cross-objective consistency: A config that appears in ALL objective survivor sets is the most defensible selection. One that appears in only one is likely an artifact of that objective's bias.
P6 - Binding Metrics Identification
After optimization, identify binding metrics - those that would increase the intersection if relaxed to 100%. Non-binding metrics are either already loose or perfectly correlated with a binding metric.
For each metric with cutoff < 100:
Relax this metric to 100, keep others fixed
If intersection grows: this metric IS binding
If intersection unchanged: this metric is redundant at current cutoffsWhy this matters: Binding metrics are the actual constraints on your quality frontier. Effort to improve configs should focus on binding dimensions.
P7 - Inert Dimension Detection
A metric is inert if it provides zero discrimination across the population. Detect this before optimization to reduce dimensionality.
If max(metric) == min(metric) across all configs: INERT
If percentile spread < 5 points: NEAR-INERTAction: Remove inert metrics from the search space or permanently set their cutoff to 100. Including them wastes optimization budget.
P8 - Forensic Post-Analysis
After optimization, perform forensic analysis to extract actionable insights:
1. Universal champions - configs surviving ALL objectives 2. Feature frequency - which features appear most in survivors 3. Metric binding sequence - order in which metrics become binding as cutoffs tighten 4. Tightening curve - intersection size vs uniform cutoff (100% -> 5%) 5. Metric discrimination power - which metric kills the most configs at each tightening step
---
Architecture Pattern
Metric JSONL files (pre-computed)
|
v
MetricSpec Registry <-- Defines name, direction, source, cutoff var
|
v
Percentile Ranker <-- scipy.stats.rankdata, None->0, flip lower-is-better
|
v
Per-Metric Cutoff <-- Each metric independently filtered
|
v
Intersection <-- Configs passing ALL cutoffs
|
v
Evolutionary Search <-- Optuna TPE/NSGA-II tunes cutoffs
|
v
Forensic Analysis <-- Cross-objective consistency, binding metricsMetricSpec Registry
The registry is the single source of truth for metric definitions. Each entry is a frozen dataclass:
@dataclass(frozen=True)
class MetricSpec:
name: str # Internal key (e.g., "tamrs")
label: str # Display label (e.g., "TAMRS")
higher_is_better: bool # Direction for percentile ranking
default_cutoff: int # Default percentile cutoff (100 = no filter)
source_file: str # JSONL filename containing raw values
source_field: str # Field name in JSONL recordsDesign principle: Adding a new metric = adding one MetricSpec entry. No other code changes required. The ranking, cutoff, intersection, and optimization machinery is fully generic.
Env Var Convention
Each metric's cutoff is controlled by a namespaced environment variable:
RBP_RANK_CUT_{METRIC_NAME_UPPER} = integer [0, 100]This enables:
- Shell-level override without code changes
- Copy-paste of optimizer output directly into next run
- CI/CD integration via environment configuration
- Mise task integration via
[env]blocks
---
Evolutionary Optimizer Design
Sampler Selection
| Scenario | Sampler | Why |
|---|---|---|
| Single-objective | TPE (Tree-Parzen Estimator) | Bayesian, handles integer/categorical, good for 10-20 dimensions |
| Multi-objective (2+) | NSGA-II | Pareto-frontier discovery, population-based |
Determinism: Always seed the sampler (seed=42). Optimization results must be reproducible.
Search Space Design
def suggest_cutoffs(trial):
cutoffs = {}
for spec in metric_registry:
cutoffs[spec.name] = trial.suggest_int(spec.name, 5, 100, step=5)
return cutoffsWhy step=5: Reduces the search space by 20x (20 values per metric vs 100) while maintaining sufficient granularity. For 12 metrics, this is 20^12 = 4 x 10^15 vs 100^12 = 10^24.
Why lower bound = 5: cutoff=0 always produces empty intersection. Values below 5 are too stringent to be useful in practice.
Data Pre-Loading (Critical Performance Pattern)
# Load metric data ONCE, share across all trials
metric_data = load_metric_data(results_dir, metric_registry)
def objective(trial):
cutoffs = suggest_cutoffs(trial)
# Pass pre-loaded data - avoids disk I/O per trial
result = run_ranking_with_cutoffs(cutoffs, metric_data=metric_data)
return obj_fn(result, cutoffs)Why: Each trial evaluates in ~6ms when data is pre-loaded (pure NumPy/set operations). Without pre-loading, each trial incurs ~50ms of disk I/O. At 10,000 trials, this is 60 seconds vs 500 seconds.
Objective Function Patterns
Pattern 1 - Ratio Optimization
def obj_max_survivors_min_cutoff(result, cutoffs):
n = result["n_intersection"]
if n == 0:
return 0.0
mean_cutoff = sum(cutoffs.values()) / len(cutoffs)
return n / mean_cutoff # More survivors per unit of loosenessUse when: Exploring the efficiency frontier - how much quality can you get for how much filtering?
Pattern 2 - Constrained Quality
def obj_quality_at_target_n(result, cutoffs, target_n=10):
n = result["n_intersection"]
avg_pct = result["avg_percentile"]
if n < target_n:
return avg_pct * (n / target_n) # Partial credit
return avg_pct # Full credit: maximize qualityUse when: You have a target portfolio size and want the highest quality subset.
Pattern 3 - Minimum Budget
def obj_tightest_nonempty(result, cutoffs):
n = result["n_intersection"]
if n == 0:
return 0.0
total_budget = sum(cutoffs.values())
return max_possible_budget - total_budget # Lower budget = betterUse when: Finding the single most universally excellent config.
Pattern 4 - Diversity Reward
def obj_diversity_reward(result, cutoffs):
n = result["n_intersection"]
if n == 0:
return 0.0
n_binding = result["n_binding_metrics"]
n_active = sum(1 for v in cutoffs.values() if v < 100)
if n_active == 0:
return 0.0
efficiency = n_binding / n_active
return n * efficiencyUse when: Ensuring that tightened cutoffs provide independent information, not redundant filtering.
Pattern 5 - Pareto (Multi-Objective)
study = optuna.create_study(
directions=["maximize", "minimize"], # max survivors, min cutoff
sampler=optuna.samplers.NSGAIISampler(seed=42),
)
def objective(trial):
cutoffs = suggest_cutoffs(trial)
result = run_ranking_with_cutoffs(cutoffs, metric_data=metric_data)
return result["n_intersection"], sum(cutoffs.values()) / len(cutoffs)Use when: You want to see the full trade-off landscape between two competing objectives.
---
Forensic Analysis Protocol
After running all objectives, perform this analysis:
Step 1 - Cross-Objective Survivor Sets
For each objective:
survivors_{objective} = set of configs in final intersection
universal_champions = survivors_1 AND survivors_2 AND ... AND survivors_KIf a config survives all K objective functions, it is robust to objective choice.
Step 2 - Feature Theme Extraction
Count feature appearances across all survivors:
feature_counts = Counter()
for config_id in quality_survivors:
for feature in config_id.split("__"):
feature_counts[feature.split("_")[0]] += 1Dominant features reveal the underlying market microstructure that the ranking system is selecting for.
Step 3 - Uniform Tightening Curve
Apply the same cutoff to ALL metrics and plot intersection size:
@100%: 1008 survivors (no filter)
@80%: 502 survivors
@60%: 210 survivors
@40%: 68 survivors
@20%: 12 survivors
@10%: 3 survivors
@5%: 0 survivorsThe shape of this curve reveals whether the metric space has natural clusters or is uniformly distributed.
Step 4 - Binding Sequence
Tighten uniformly and at each step identify which metric was the "tightest killer" - the metric that eliminated the most configs:
@90%: 410 survivors | tightest killer: rachev (-57)
@80%: 132 survivors | tightest killer: headroom (-27)
@70%: 29 survivors | tightest killer: n_trades (-12)
@60%: 6 survivors | tightest killer: dsr (-6)This reveals the binding constraint hierarchy.
---
Implementation Checklist
When implementing this methodology in a new domain:
1. [ ] Define MetricSpec registry (name, direction, source, default cutoff) 2. [ ] Implement percentile ranking (scipy.stats.rankdata) 3. [ ] Implement per-metric cutoff application 4. [ ] Implement set intersection across all metrics 5. [ ] Add env var override for each cutoff 6. [ ] Create run_ranking_with_cutoffs() API function 7. [ ] Add binding metric detection 8. [ ] Create tightening analysis function 9. [ ] Write markdown report generator 10. [ ] Add Optuna optimizer with at least 3 objective functions 11. [ ] Pre-load metric data for optimizer performance 12. [ ] Run 5-objective forensic analysis (10K+ trials per objective) 13. [ ] Extract universal champions (cross-objective consistency) 14. [ ] Identify inert dimensions (remove from search space) 15. [ ] Document binding constraint sequence 16. [ ] Record feature themes in survivors
---
Anti-Patterns
| Anti-Pattern | Symptom | Fix | Severity |
|---|---|---|---|
| Weighted-sum scoring | Single metric dominates, others ignored | Use intersection (P3) | CRITICAL |
| Starting strict | Miss global optimum, premature convergence | Start at 100%, tighten (P4) | HIGH |
| Uniform cutoffs only | Over-filters inert metrics, under-filters binding ones | Per-metric independent cutoffs (P2) | HIGH |
| Single objective | Artifact of objective bias | Run 5+ objectives, check consistency (P5) | HIGH |
| Raw value comparison | Scale-dependent, misleading | Always use percentile ranks (P1) | HIGH |
| Including inert metrics | Wastes optimization budget | Detect and remove inert dimensions (P7) | MEDIUM |
| No data pre-loading | Optimizer 10x slower | Pre-load once, share across trials | MEDIUM |
| Unseeded optimizer | Non-reproducible results | Always seed sampler (seed=42) | MEDIUM |
| Missing forensic analysis | Raw numbers without insight | Run full forensic protocol (P8) | MEDIUM |
---
References
| Topic | Reference File |
|---|---|
| Range Bar Case Study | case-study-rangebar-ranking.md |
| Objective Functions | objective-functions.md |
| Metric Design Guide | metric-design-guide.md |
Related Skills
| Skill | Relationship |
|---|---|
| rangebar-eval-metrics | Metric definitions (TAMRS, Omega, DSR, etc.) fed into ranking |
| adaptive-wfo-epoch | Walk-Forward metrics that could be ranked |
| backtesting-py-oracle | Validates trade outcomes used in metric computation |
| sharpe-ratio-non-iid-corrections | DSR computation with non-IID corrections |
Dependencies
pip install scipy numpy optuna>=4.7---
TodoWrite Task Templates
Template A - Implement Ranking System (New Project)
1. [Preflight] Identify all evaluation metrics and their JSONL sources
2. [Preflight] Define MetricSpec registry (name, direction, source_file, source_field)
3. [Execute] Implement percentile_ranks() with scipy.stats.rankdata
4. [Execute] Implement apply_cutoff() and intersection()
5. [Execute] Add env var override for each metric cutoff (RANK_CUT_{NAME})
6. [Execute] Create run_ranking_with_cutoffs() API for optimizer
7. [Execute] Add binding metric detection and tightening analysis
8. [Execute] Write markdown report generator
9. [Verify] Unit tests for all pure functions (14+ tests)
10. [Verify] Run with default cutoffs (100%) - all configs should surviveTemplate B - Add Evolutionary Optimizer
1. [Preflight] Verify ranking module has run_ranking_with_cutoffs() API
2. [Preflight] Add optuna>=4.7 dependency
3. [Execute] Implement 5 objective functions
4. [Execute] Create suggest_cutoffs() with step=5 search space
5. [Execute] Pre-load metric data once, share across trials
6. [Execute] Handle pareto_efficiency (NSGA-II) as special case
7. [Execute] Write JSONL output with provenance (git commit, timestamp)
8. [Verify] POC with 10 trials - verify non-trivial cutoffs found
9. [Verify] Full run with 10K trials per objectiveTemplate C - Forensic Analysis
1. [Preflight] Collect optimization results from all 5 objectives
2. [Execute] Extract survivor sets per objective
3. [Execute] Compute cross-objective intersection (universal champions)
4. [Execute] Run uniform tightening analysis (100% -> 5%)
5. [Execute] Identify binding metrics at each tightening step
6. [Execute] Extract feature themes from quality survivors
7. [Execute] Detect inert dimensions (zero discrimination)
8. [Verify] Document findings in structured summary table---
Post-Change Checklist (Self-Maintenance)
After modifying this skill:
1. [ ] Principles P1-P8 remain internally consistent 2. [ ] Anti-patterns table covers new patterns discovered 3. [ ] References in references/ are up to date 4. [ ] Case study reflects latest production results 5. [ ] Implementation checklist is complete and ordered 6. [ ] Plugin README updated if description changed
---
Troubleshooting
| Issue | Cause | Solution |
|---|---|---|
| All cutoffs converge to 100% | Metrics are all correlated | Check for metric redundancy (Spearman r > 0.95) |
| Zero intersection at mild cutoffs | One metric has near-zero variance | Detect inert dimensions (P7) |
| Optimizer takes too long | Disk I/O per trial | Pre-load metric data (see Performance section) |
| Different objectives give same answer | Objectives poorly differentiated | Verify objective formulas test different trade-offs |
| Universal champion is mediocre | Survival != excellence | Check raw values, not just survival |
| Binding sequence changes across runs | Unseeded optimizer | Always use seed=42 |
| Too many survivors | Cutoffs too loose | Increase n_trials, lower step size |
| Zero survivors | Cutoffs too tight | Check for inert metrics inflating dimensionality |
Post-Execution Reflection
After this skill completes, reflect before closing the task:
0. Locate yourself. — Find this SKILL.md's canonical path (Glob for this skill's name) before editing. All corrections target THIS file and its sibling references/ — never other documentation. 1. What failed? — Fix the instruction that caused it. If it could recur, add it as an anti-pattern. 2. What worked better than expected? — Promote it to recommended practice. Document why. 3. What drifted? — Any script, reference, or external dependency that no longer matches reality gets fixed now. 4. Log it. — Every change gets an evolution-log entry with trigger, fix, and evidence.
Do NOT defer. The next invocation inherits whatever you leave behind
Case Study - Range Bar Pattern Ranking (Issue #17)
Production application of evolutionary metric ranking to 1,008 two-feature trading configurations evaluated across 12 quality metrics. Executed in terrylica/rangebar-patterns (2026-02-14).
Repository: terrylica/rangebar-patterns Issue: #17 - Per-Metric Percentile Cutoffs
---
Problem
1,008 trading configurations (two-feature filter combinations applied to a 2-consecutive-DOWN-bar pattern on SOLUSDT @500dbps range bars) needed to be ranked across 12 heterogeneous quality metrics. Raw values were on incompatible scales:
| Metric | Range | Scale |
|---|---|---|
| Kelly fraction | [-0.15, +0.08] | Return per unit risk |
| Trade count | [50, 3500] | Integer count |
| Omega ratio | [0.85, 1.25] | Ratio (>1 = profit) |
| TAMRS | [0.009, 0.379] | Composite score |
| Rachev ratio | [0.0, 2.0] | Tail asymmetry |
| DSR | [0.0, 0.5] | Deflated Sharpe |
| E-value | [1.0, 1.02] | Sequential test |
| Regularity CV | [0.0, 2.5] | Lower = better |
No weighted-sum scoring could meaningfully combine these. Traditional screening gates (pass/fail thresholds) were too coarse - a config just below a threshold is essentially identical to one just above.
---
Implementation
Metric Registry (12 metrics)
DEFAULT_METRICS = (
MetricSpec("tamrs", "TAMRS", True, 100, "tamrs_rankings.jsonl", "tamrs"),
MetricSpec("rachev", "Rachev", True, 100, "tamrs_rankings.jsonl", "rachev_ratio"),
MetricSpec("ou_ratio", "OU Ratio", True, 100, "tamrs_rankings.jsonl", "ou_barrier_ratio"),
MetricSpec("sl_cdar", "SL/CDaR", True, 100, "tamrs_rankings.jsonl", "sl_cdar_ratio"),
MetricSpec("omega", "Omega", True, 100, "omega_rankings.jsonl", "omega_L0"),
MetricSpec("dsr", "DSR", True, 100, "dsr_rankings.jsonl", "dsr"),
MetricSpec("headroom", "MinBTL Headroom", True, 100, "minbtl_gate.jsonl", "headroom_ratio"),
MetricSpec("evalue", "E-value", True, 100, "evalues.jsonl", "final_evalue"),
MetricSpec("regularity_cv", "Regularity CV", False, 100, "signal_regularity_rankings.jsonl", "kde_peak_cv"),
MetricSpec("coverage", "Coverage", True, 100, "signal_regularity_rankings.jsonl", "temporal_coverage"),
MetricSpec("n_trades", "Trade Count", True, 100, "moments.jsonl", "n_trades"),
MetricSpec("kelly", "Kelly", True, 100, "moments.jsonl", "kelly_fraction"),
)Note: regularity_cv has higher_is_better=False - lower CV means more regular signal timing, which is desirable.
Env Var Configuration
# Default: all cutoffs at 100% (no filter)
mise run eval:rank
# Custom cutoffs from optimizer output
RBP_RANK_CUT_TAMRS=30 RBP_RANK_CUT_RACHEV=90 RBP_RANK_CUT_OMEGA=70 \
RBP_RANK_CUT_HEADROOM=25 RBP_RANK_CUT_KELLY=35 mise run eval:rankFiles Created
| File | Lines | Purpose |
|---|---|---|
src/rangebar_patterns/eval/ranking.py | 453 | MetricSpec, percentile ranks, cutoffs, intersection, report |
scripts/rank_optimize.py | 241 | Optuna optimizer with 5 objectives |
tests/test_eval/test_ranking.py | ~130 | 14 unit tests for all pure functions |
---
Optimization Results (5 Objectives x 10,000 Trials)
Summary Table
| Objective | Survivors | Mean Cutoff | Active Filters | Key Insight |
|---|---|---|---|---|
| max_survivors_min_cutoff | 835 | 94.6% | 4/12 | Barely filters - DSR, headroom, kelly, ou_ratio slightly tightened |
| quality_at_target_n | 11 | 66.7% | 11/12 | Best balance: 11 high-quality configs, 72.3% avg percentile |
| tightest_nonempty | 1 | 31.3% | 12/12 | Maximum tightening - sole universal champion |
| diversity_reward | 747 | 97.5% | 4/12 | Rewards independent metrics - evalue, n_trades, omega, ou_ratio |
| pareto_efficiency | 348 | 85.8% | 10/12 | 353-point Pareto front from NSGA-II |
Optimal Cutoffs (quality_at_target_n)
RBP_RANK_CUT_TAMRS=30 RBP_RANK_CUT_RACHEV=90 RBP_RANK_CUT_OU_RATIO=60
RBP_RANK_CUT_SL_CDAR=50 RBP_RANK_CUT_OMEGA=70 RBP_RANK_CUT_DSR=95
RBP_RANK_CUT_HEADROOM=25 RBP_RANK_CUT_EVALUE=95 RBP_RANK_CUT_REGULARITY_CV=65
RBP_RANK_CUT_COVERAGE=85 RBP_RANK_CUT_N_TRADES=100 RBP_RANK_CUT_KELLY=35This produces 11 survivors with average percentile rank 72.35% across all metrics.
---
Universal Champion
`turnover_imbalance_lt_p25__price_impact_lt_p25` - the ONLY config appearing in all 5 objective survivor sets.
| Metric | Value |
|---|---|
| Kelly | +0.051 |
| Omega | 1.236 |
| TAMRS | 0.078 |
| Rachev | 2.000 (saturated) |
| OU ratio | 0.392 |
| N trades | 131 |
| KDE peak CV | 0.000 (perfectly regular) |
| E-value | 1.006 |
Interpretation: This config selects moments where both turnover imbalance and price impact are in the bottom quartile of signal-specific rolling distributions. Low turnover imbalance + low price impact = informed flow absorbing liquidity before a move.
---
Key Forensic Findings
Finding 1 - DSR is an Inert Dimension
DSR max across ALL 961 configs: 0.500. Zero configs above 0.50. Zero configs above 0.95.
DSR is a flat-zero field that cannot discriminate between configs. The effective ranking space is 11-dimensional, not 12. DSR should be removed or permanently set to cutoff=100.
Root cause: None of the 1,008 configs produce statistically significant results under Deflated Sharpe Ratio with the null inflation from 1,008 trials. This is a feature (correct multiple testing), not a bug.
Finding 2 - Binding Constraint Hierarchy
Uniform cutoff tightening reveals the binding sequence:
@90%: 410 survivors | tightest killer: rachev (-57)
@80%: 132 survivors | tightest killer: headroom (-27)
@70%: 29 survivors | tightest killer: n_trades (-12)
@60%: 6 survivors | tightest killer: dsr (-6) [artifact - kills 6 because of zero-mass tail]
@50%: 0 survivorsBinding constraints: Rachev ratio and MinBTL headroom are the primary quality gates. Configs with strong Rachev (tail asymmetry) and sufficient data (headroom above MinBTL) are the rarest combinations.
Finding 3 - Feature Themes in Quality Survivors
The 11 quality survivors (quality_at_target_n objective) show dominant features:
| Feature | Count (out of 11) | Interpretation |
|---|---|---|
| OFI (order flow imbalance) | 5 | Directional pressure |
| turnover_imbalance | 4 | Liquidity asymmetry |
| price_impact | 4 | Market impact costs |
| vwap_close_deviation | 4 | Institutional execution |
Theme: Order flow imbalance + price impact asymmetry. These configs detect moments where order flow is extreme AND price impact is low - consistent with informed flow absorbing liquidity before a move.
Finding 4 - Performance Characteristics
- Single-core optimal: Each evaluation takes ~6.3ms (pure NumPy/set operations). Data fits in L2 cache. TPE sampler is inherently sequential.
- 10K trials per objective: ~60 seconds each, 5 objectives = 5 minutes total.
- Apple Silicon: M-series chips handle this workload trivially. No GPU or parallelism needed.
- Memory: <100MB total (12 metric files, ~1000 configs each).
---
Lessons Learned
L1 - Pre-compute All Metrics Before Ranking
The ranking system reads pre-computed JSONL files. Each metric module (TAMRS, Rachev, Omega, etc.) runs independently and writes its own JSONL. The ranking module never computes metrics - it only reads and ranks.
Why: Decoupling computation from ranking means the optimizer can run 10K+ trials at ~6ms each without touching the database.
L2 - Optuna TPE is Sufficient for This Scale
With 12 integer dimensions (step=5, 20 values each), TPE finds good solutions in 200-500 trials and plateaus by 2,000. Running 10,000 trials is overkill but confirms convergence. Grid search over 20^12 = 4 x 10^15 would be infeasible; random search would need ~50K trials.
L3 - Cross-Objective Consistency is the Strongest Filter
The universal champion (turnover_imbalance_lt_p25__price_impact_lt_p25) would not have been identified by any single objective alone. It ranks 1st under tightest_nonempty but only ~8th under quality_at_target_n. Cross-objective intersection reveals robustness that single-objective optimization misses.
L4 - Inert Dimensions Waste Budget
DSR contributed zero discrimination but consumed one of 12 cutoff dimensions. Detecting and removing it before optimization would have made the search more efficient. In general, run a quick inertness check before any optimization.
L5 - The Ranking System Coexists with Screening
The per-metric percentile ranking system was built parallel to the existing multi-tier screening system (screening.py). Neither replaces the other:
- Screening (pass/fail gates): "Does this config meet minimum standards?"
- Ranking (percentile cutoffs): "Among all configs, which are consistently excellent?"
Both provide valid but different perspectives on the same data.
---
Reproduction
# In terrylica/rangebar-patterns:
# 1. Run the full eval pipeline (requires ClickHouse)
mise run eval:full
# 2. Run ranking with default cutoffs
mise run eval:rank
# 3. Run evolutionary optimizer (all 5 objectives)
for OBJ in max_survivors_min_cutoff quality_at_target_n tightest_nonempty diversity_reward pareto_efficiency; do
RBP_RANK_OBJECTIVE=$OBJ RBP_RANK_N_TRIALS=10000 mise run eval:rank-optimize
done
# 4. Apply best cutoffs and inspect
RBP_RANK_CUT_TAMRS=30 RBP_RANK_CUT_RACHEV=90 RBP_RANK_CUT_OMEGA=70 \
RBP_RANK_CUT_HEADROOM=25 RBP_RANK_CUT_KELLY=35 mise run eval:rank---
Output Artifacts
| File | Format | Content |
|---|---|---|
results/eval/rankings.jsonl | 1 line per config | Percentile ranks across all 12 metrics |
results/eval/ranking_report.md | Markdown | Human-readable report with top configs |
results/eval/rank_optimization.jsonl | 1 line per objective | Best cutoffs, survivor counts, env vars |
Evolution Log
Convention: Reverse chronological order (newest on top, oldest at bottom). Prepend new entries.
---
2026-02-26: Initial Evolution Log
Status: Skill is in use and maintained. Track improvements here.
Purpose
This evolution log tracks updates to the skill. Each entry should note:
- What changed (content, structure, tooling)
- Why it changed (bug fix, feature request, best practice)
- Files affected
How to Use
1. When updating SKILL.md or references, add an entry here with the date 2. Keep entries reverse-chronological (newest first) 3. Link to ADRs or GitHub issues when relevant 4. Reference specific line changes when helpful
---
Metric Design Guide
How to design metrics that work well with evolutionary percentile ranking. A metric that is good for single-config evaluation may be poor for cross-config ranking.
---
Metric Quality Criteria for Ranking
Criterion 1 - Discrimination Power
A metric must spread configs across a meaningful range. If 95% of configs cluster at the same value, the metric provides almost no ranking information.
Test: Compute the interquartile range (IQR) as a fraction of the full range. If IQR/range < 0.1, the metric has weak discrimination.
Example: DSR in the rangebar case study had max=0.500 across 961 configs. Zero configs exceeded 0.50. The metric was inert (zero discrimination power). This wasted one dimension of the optimization search space.
Criterion 2 - Independence from Other Metrics
Highly correlated metrics (Spearman r > 0.95) provide redundant information. Including both inflates the dimensionality without adding discriminatory value.
Test: Compute Spearman rank correlation between all metric pairs. Flag pairs with |r| > 0.95 as redundant. Keep the one with better discrimination power.
Example: Sharpe, PSR, GROW, and CF-ES were dropped from the rangebar metric set because they had r > 0.95 with Omega ratio.
Criterion 3 - Monotonic in Quality
The metric should have a clear direction. "Higher is better" or "lower is better" must be unambiguous. Metrics with non-monotonic quality (e.g., "closer to 1.0 is better") need transformation before ranking.
Transformation: For target-value metrics, use abs(value - target) and set higher_is_better=False.
Criterion 4 - Defined for All Configs
None/NaN values get percentile 0 (worst). If many configs produce None for a metric, that metric effectively creates a binary gate (defined vs undefined) rather than a continuous ranking.
Guideline: If >30% of configs have None, the metric is better suited as a pre-filter (gate) than a ranking dimension.
Criterion 5 - Robust to Outliers
A single extreme value can distort percentile ranks for neighboring configs. Use rankdata(method='average') (ties get the mean rank) and consider winsorizing extreme values before ranking.
---
MetricSpec Design Patterns
Pattern - Direct Metric
The simplest case. The JSONL file contains the raw metric value.
MetricSpec("omega", "Omega", True, 100, "omega_rankings.jsonl", "omega_L0")Pattern - Inverse Metric (Lower is Better)
Set higher_is_better=False. The ranking module will negate values before ranking, so the config with the lowest raw value gets percentile 100.
MetricSpec("regularity_cv", "Regularity CV", False, 100,
"signal_regularity_rankings.jsonl", "kde_peak_cv")Pattern - Composite Metric
The source field is itself a composite (e.g., TAMRS = Rachev _SL/CDaR_ OU ratio). Include the composite AND its components as separate metrics. This lets the optimizer tighten on the composite or its components independently.
# Composite
MetricSpec("tamrs", "TAMRS", True, 100, "tamrs_rankings.jsonl", "tamrs"),
# Components (also available for independent filtering)
MetricSpec("rachev", "Rachev", True, 100, "tamrs_rankings.jsonl", "rachev_ratio"),
MetricSpec("sl_cdar", "SL/CDaR", True, 100, "tamrs_rankings.jsonl", "sl_cdar_ratio"),
MetricSpec("ou_ratio", "OU Ratio", True, 100, "tamrs_rankings.jsonl", "ou_barrier_ratio"),Pattern - Count Metric
Integer counts (e.g., trade count) have many ties. rankdata(method='average') handles this correctly, but consider whether the count is better as a gate than a ranking dimension.
MetricSpec("n_trades", "Trade Count", True, 100, "moments.jsonl", "n_trades")Pattern - Multi-Source Metric
When a metric requires data from multiple JSONL files, pre-compute it into a single JSONL file. The ranking module reads exactly one file per metric.
---
Metric Count Guidelines
| Metric Count | Search Space | Trials Needed | Recommended |
|---|---|---|---|
| 5-8 | 20^5 to 20^8 | 200-1000 | Good |
| 9-12 | 20^9 to 20^12 | 1000-5000 | Typical |
| 13-16 | 20^13 to 20^16 | 5000-10000 | Max practical |
| 17+ | 20^17+ | >50000 | Split into stages |
Why 12 is near-optimal: With step=5 (20 values per metric), 12 metrics create a 20^12 search space. TPE converges reliably within 5000-10000 trials at this dimensionality. Beyond 16 metrics, consider hierarchical optimization (optimize subgroups, then combine).
---
Adding a New Metric
1. Compute the metric in its own module, writing results to JSONL with config_id and metric value 2. Add a MetricSpec entry to the registry 3. Add an env var (RANK_CUT_{NAME}) to config.py with default=100 4. Add to resolve_cutoffs() mapping 5. Run inertness check: If the new metric has IQR/range < 0.1, reconsider including it 6. Run correlation check: If Spearman r > 0.95 with an existing metric, keep the more discriminating one 7. Re-run optimization: New metric changes the search space
---
Removing a Metric
Removing a metric (setting its cutoff permanently to 100 or removing from the registry) is often more valuable than adding one. Signs a metric should be removed:
1. Inert: Max value == min value (or IQR/range < 0.05) 2. Redundant: Spearman r > 0.95 with a more discriminating metric 3. Binary gate: >30% None values (better as pre-filter) 4. Non-binding: In all 5 optimization objectives, the metric's cutoff stays at 100%
Do NOT remove a metric just because it is "hard to improve" - binding metrics are the most valuable ranking dimensions.
Objective Function Reference
Detailed guide for designing and selecting objective functions for evolutionary cutoff optimization. Each objective encodes a different definition of "quality" and reveals different aspects of the configuration landscape.
---
Design Principles
Principle 1 - Handle Empty Intersection
Every objective must return 0 (or equivalent worst value) when n_intersection == 0. This prevents the optimizer from exploring the empty-intersection region of the search space.
if result["n_intersection"] == 0:
return 0.0 # Worst possible valuePrinciple 2 - Monotonic in Quality
The objective should increase monotonically with the quality being measured. Optuna maximizes by default; for minimization objectives, return the complement.
# Minimize total budget -> maximize (max_budget - budget)
return max_budget - total_budgetPrinciple 3 - Meaningful Gradients
The objective should change smoothly with cutoff changes. Discontinuous objectives (like "1 if survivors >= 10, else 0") provide no gradient for the optimizer to follow.
# GOOD: Partial credit for n < target
if n < target_n:
return avg_pct * (n / target_n) # Smooth degradation
# BAD: Cliff function
if n < target_n:
return 0.0 # No gradient---
Objective Catalog
1. max_survivors_min_cutoff (Efficiency Frontier)
Question: How many configs survive per unit of filtering looseness?
def obj_max_survivors_min_cutoff(result, cutoffs):
n = result["n_intersection"]
if n == 0:
return 0.0
mean_cutoff = sum(cutoffs.values()) / len(cutoffs)
if mean_cutoff < 1:
return 0.0
return n / mean_cutoffBehavior: Favors loose cutoffs that keep many survivors. The optimizer finds cutoffs just tight enough to provide meaningful filtering while maximizing the survivor count.
Typical result: Large survivor sets (hundreds) with most cutoffs near 100%. Only the most discriminating metrics get slightly tightened.
Use when: Exploring the efficiency frontier, understanding which metrics provide the most bang-for-buck filtering.
2. quality_at_target_n (Constrained Portfolio)
Question: Given a target portfolio size N, what cutoffs maximize the average quality of survivors?
def obj_quality_at_target_n(result, cutoffs, target_n=10):
n = result["n_intersection"]
avg_pct = result["avg_percentile"]
if n < target_n:
return avg_pct * (n / target_n) # Partial credit
return avg_pctBehavior: Tightens cutoffs aggressively to select the highest-quality subset of exactly N configs. The partial credit term prevents the optimizer from converging on n=0 (which has undefined quality).
Typical result: Moderate survivor count (close to target_n) with high average percentile. Most metrics have meaningful cutoffs.
Tuning: Set target_n based on your deployment capacity. For a 10-strategy portfolio, use target_n=10. For initial screening, use target_n=50.
Use when: You know how many configs you want to deploy and want the best possible set.
3. tightest_nonempty (Universal Champion)
Question: What is the absolute tightest set of cutoffs that still yields at least one survivor?
def obj_tightest_nonempty(result, cutoffs):
n = result["n_intersection"]
if n == 0:
return 0.0
total_budget = sum(cutoffs.values())
max_budget = len(cutoffs) * 100
return max_budget - total_budgetBehavior: Drives all cutoffs as low as possible while maintaining at least one survivor. This finds the single config (or small set) that is excellent across the most dimensions simultaneously.
Typical result: 1-3 survivors with very tight cutoffs (mean ~30%). The survivor is the closest thing to a "universal champion."
Interpretation warning: The sole survivor may not be the best on any single metric - it is the most consistently good across all metrics. Check its raw values to ensure they meet minimum requirements.
Use when: Finding the single most defensible config selection.
4. pareto_efficiency (Multi-Objective Trade-Off)
Question: What is the full trade-off landscape between survivor count and cutoff tightness?
study = optuna.create_study(
directions=["maximize", "minimize"],
sampler=optuna.samplers.NSGAIISampler(seed=42),
)
def pareto_objective(trial):
cutoffs = suggest_cutoffs(trial)
result = run_ranking_with_cutoffs(cutoffs, metric_data=metric_data)
return result["n_intersection"], sum(cutoffs.values()) / len(cutoffs)Behavior: Returns the full Pareto frontier - the set of solutions where you cannot improve one objective without worsening the other. Each point on the frontier represents a different quality/quantity trade-off.
Typical result: 100-500 Pareto-optimal solutions spanning from "many survivors, loose cutoffs" to "few survivors, tight cutoffs."
Reading the frontier: Plot survivors (y-axis) vs mean cutoff (x-axis). Look for "knees" where the curve bends sharply - these are natural transition points.
Use when: You want to understand the full landscape before committing to a specific operating point.
5. diversity_reward (Redundancy Detector)
Question: Are all tightened cutoffs providing independent information, or are some redundant?
def obj_diversity_reward(result, cutoffs):
n = result["n_intersection"]
if n == 0:
return 0.0
n_binding = result["n_binding_metrics"]
n_active = sum(1 for v in cutoffs.values() if v < 100)
if n_active == 0:
return 0.0
efficiency = n_binding / n_active
return n * efficiencyBehavior: Penalizes cutoff combinations where some tightened metrics are redundant (not binding). A metric is "not binding" if relaxing it to 100% does not change the intersection.
Typical result: Fewer active filters than other objectives, but each active filter genuinely matters. Reveals which metrics are correlated (tightening one makes the other redundant).
Use when: Designing a metric dashboard, deciding which metrics to invest effort in improving, or identifying metric redundancy.
---
Objective Selection Guide
| Goal | Recommended Objective | Why |
|---|---|---|
| Initial exploration | max_survivors_min_cutoff | See the landscape |
| Deployment selection | quality_at_target_n | Matches real-world constraint |
| Academic publication | tightest_nonempty + pareto | Defensible methodology |
| Metric design | diversity_reward | Reveals redundancy |
| Full analysis | ALL FIVE + cross-objective | Most robust conclusions |
---
Custom Objective Design
When the 5 built-in objectives don't fit, design a custom one following these patterns:
Pattern - Weighted Quality
def obj_weighted_quality(result, cutoffs, weights=None):
"""Weight some metrics more than others in quality assessment."""
n = result["n_intersection"]
if n == 0:
return 0.0
weighted_sum = 0.0
for metric_name, pct_ranks in result["all_pct_ranks"].items():
w = weights.get(metric_name, 1.0) if weights else 1.0
for cid in result["survivors"]:
weighted_sum += w * pct_ranks.get(cid, 0.0)
return weighted_sum / (n * sum(weights.values()))Pattern - Stability Reward
def obj_stability_reward(result, cutoffs, reference_cutoffs=None):
"""Prefer cutoffs close to a reference point (prior knowledge)."""
n = result["n_intersection"]
if n == 0:
return 0.0
distance = sum(
abs(cutoffs[k] - reference_cutoffs.get(k, 50))
for k in cutoffs
)
return n / (1 + 0.01 * distance)Pattern - Minimum Per-Metric Quality
def obj_min_percentile(result, cutoffs):
"""Maximize the WORST percentile across survivors (maximin)."""
n = result["n_intersection"]
if n == 0:
return 0.0
min_pct = float("inf")
for cid in result["survivors"]:
for pct_ranks in result["all_pct_ranks"].values():
min_pct = min(min_pct, pct_ranks.get(cid, 0.0))
return min_pct # Higher = better worst-case