I1 (crash): zero-sum HRP ZeroDivisionError — `hrp_sum = sum(...) or 0.0` was a no-op since `0.0 or 0.0 == 0.0`. Guard BOTH divide sites in `_size_active`: all-allocated branch and mixed branch now fall back to equal-weight when `hrp_sum <= 0`. I2 (honest dry-run): `factory-cycle --dry-run` previously ran the full discovery chain (gather+hunt+forward) and wrote to the store (mark_seen, upsert DISCOVERED, set_status RETIRED). Gate the entire GENERATE→JUDGE→PROMOTE→PERSIST block behind `if execute:`. In dry-run, only the FactoryLoop runs in kill-gated mode (promotes nothing) and reports the current pool state without any store writes. M1 (double-count + dedup namespace split): analyst fleet AND hunt each called `add_trials`, and used different key formats (`market:kind:k=v` vs `kind|market|params`). Fix: - `candidate_key(market, kind, params)` added to `fxhnt.domain.factory` as the single canonical key helper (format: `market:kind:k1=v1,k2=v2` — params sorted, 10g floats). - `Proposal.key()` delegates to `candidate_key` (unified namespace). - `FleetOrchestrator.hunt` uses `candidate_key` for its `cand_key` (was `kind|sym|params`). - `AnalystFleet.gather` is now read-only: no `add_trials`, no `mark_seen`. The hunt is the single counting authority. gather only filters proposals already in `store.is_seen`. M2 (funnel double-count): `forward` was sampled BEFORE `loop.allocate_and_promote()` so sleeves promoted this cycle appeared in both `forward` and `deployed`. Now `forward` is computed as `fwd_report.forward_tracking - len(res.promoted)` after promotion. M3 (E501): wrap `assemble` signature in `regime_execution.py` (was 123 chars). Pre-existing lint (UP042, E501) in `domain/factory/models.py` also fixed: `StrategyStatus(str, Enum)` → `StrategyStatus(StrEnum)`; `forward_returns` comment moved above the field to fit in 120 chars. Tests: 246 pass (was 241). New tests in test_regime_execution.py: test_hrp_zero_weights_all_allocated_no_crash test_hrp_zero_weight_single_leg_no_crash test_hrp_zero_weights_mixed_branch_no_crash Updated test_analyst_fleet.py: dedup-only contract assertions (trials==0, seen==0 after gather; fresh proposals not pre-emptively marked seen; canonical key format check). Updated test_factory_cycle_wiring.py: aligned with M1 — gather is read-only, hunt is the single counting authority. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fxhnt
Agentic strategy-research & multi-strategy execution platform. It systematically discovers, backtests and out-of-sample-validates trading strategies across many markets, keeps only what survives a rigorous statistical gauntlet, and runs the survivors live (multiple strategies at once).
The bet is not a secret edge — it's breadth + discipline + automation. The hard part (and the moat) is refusing to fool yourself at scale; the validation gauntlet is the core, built and proven first.
Architecture (hexagonal / ports-and-adapters)
src/fxhnt/
domain/ pure logic: gauntlet (Deflated Sharpe), strategies, backtest, models ← no I/O
ports/ contracts: DataProvider, repositories (the only seams)
adapters/ infra: yahoo data, SQLAlchemy (Postgres/SQLite) + DuckDB stores
application/ use-case services (ResearchService) — orchestrate via ports
cli.py composition root (wires concrete adapters)
See docs/architecture/0001-architecture.md.
Quickstart
pip install -e ".[dev]"
pytest # unit (gauntlet falsification) + integration (vertical slice)
fxhnt strategies # list strategy kinds
fxhnt research SPY --kind trend --window 200 # data → backtest → gauntlet → persist
fxhnt list --passed-only # the survivor library
Config via FXHNT_* env vars (e.g. FXHNT_OPERATIONAL_DSN=postgresql+psycopg://...). Defaults to
SQLite + a local DuckDB file under ~/.fxhnt/.
Status
Vertical slice working: data (Yahoo) → strategy (trend) → backtest (net of costs) → IS/OOS gauntlet → persistence (operational + analytical). Next: the multi-strategy execution layer, more strategy templates + data adapters, and the agentic discovery search on top of the proven gauntlet.