quantlab-factor-agent 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {quantlab_factor_agent-0.1.0/quantlab_factor_agent.egg-info → quantlab_factor_agent-0.2.0}/PKG-INFO +45 -3
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/README.md +44 -2
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/__init__.py +1 -1
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/_admission.py +5 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/_feedback.py +37 -21
- quantlab_factor_agent-0.2.0/factor_agent/_lineage.py +105 -0
- quantlab_factor_agent-0.2.0/factor_agent/_trial_graph.py +386 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/campaign.py +361 -44
- quantlab_factor_agent-0.2.0/factor_agent/cli.py +48 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/config.py +191 -14
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/events.py +9 -2
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/llm/anthropic.py +4 -1
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/llm/base.py +49 -16
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/llm/claude_code.py +4 -1
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/llm/openai_compatible.py +4 -2
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/llm/replay.py +3 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/llm/scripted.py +3 -0
- quantlab_factor_agent-0.2.0/factor_agent/preflight.py +94 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/prompts.py +27 -6
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/researcher.py +38 -11
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/snapshot.py +53 -27
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/web/_ledger.py +12 -0
- quantlab_factor_agent-0.2.0/factor_agent/web/_plans.py +268 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/web/_round.py +8 -3
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/web/app.py +13 -1
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/web/run_view.py +17 -3
- quantlab_factor_agent-0.2.0/factor_agent/web/static/assets/index-C1Tg3uN3.css +1 -0
- quantlab_factor_agent-0.2.0/factor_agent/web/static/assets/index-Dvvznupv.js +74 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/web/static/index.html +2 -2
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/pyproject.toml +2 -1
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0/quantlab_factor_agent.egg-info}/PKG-INFO +45 -3
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/quantlab_factor_agent.egg-info/SOURCES.txt +12 -2
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/quantlab_factor_agent.egg-info/entry_points.txt +1 -0
- quantlab_factor_agent-0.2.0/tests/test_campaign_check.py +318 -0
- quantlab_factor_agent-0.2.0/tests/test_campaign_lineage.py +338 -0
- quantlab_factor_agent-0.2.0/tests/test_campaign_picked_parents.py +263 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_config.py +116 -1
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_gate.py +3 -2
- quantlab_factor_agent-0.2.0/tests/test_import.py +7 -0
- quantlab_factor_agent-0.2.0/tests/test_lineage.py +89 -0
- quantlab_factor_agent-0.2.0/tests/test_run_lineage.py +172 -0
- quantlab_factor_agent-0.2.0/tests/test_trial_graph.py +259 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_web.py +74 -2
- quantlab_factor_agent-0.1.0/factor_agent/web/static/assets/index-9QjjJgl3.css +0 -1
- quantlab_factor_agent-0.1.0/factor_agent/web/static/assets/index-iOE9ei_S.js +0 -74
- quantlab_factor_agent-0.1.0/tests/test_campaign_lineage.py +0 -179
- quantlab_factor_agent-0.1.0/tests/test_import.py +0 -5
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/LICENSE +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/KUNQUANT_LICENSE +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/_acceptance_model.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/_expression_fold.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/_max_drawdown.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/_model_cache.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/_scheduler.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/acceptance.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/accepted/__init__.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/aggregations.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/candidate.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/derived.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/evaluator.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/exported.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/gate/__init__.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/gate/_canonical.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/gate/_checks.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/gate/_constraints.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/gate/_dimension.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/gate/_originality.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/gate/_sandbox.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/gate/graph.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/gate/stages.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/gate/vocabulary.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/ledger.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/llm/__init__.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/llm/_common.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/llm/_raw.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/llm/log.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/period_lag.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/scoring.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/segments.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/session.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/web/__init__.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/web/_acceptance.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/web/_config.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/web/_diagnosis.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/web/_files.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/web/_health.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/web/_ic.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/web/_memory.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/web/_results.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/web/_trials.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/zoo.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/quantlab_factor_agent.egg-info/dependency_links.txt +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/quantlab_factor_agent.egg-info/requires.txt +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/quantlab_factor_agent.egg-info/top_level.txt +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/setup.cfg +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_acceptance.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_admission_threshold.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_campaign.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_campaign_loop.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_campaign_prompts.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_confirmation.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_derived_columns.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_evaluator.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_events.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_expression_fold.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_high_frequency_family.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_intraday_acceptance.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_intraday_example.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_intraday_family.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_intraday_fields.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_intraday_gate.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_intraday_prompt.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_intraday_vocabulary.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_intraday_windows.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_llm_backends.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_max_drawdown.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_period_lag.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_proposal_groups.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_run_acceptance.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_run_config.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_run_diagnosis.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_run_factors.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_run_health.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_run_ic.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_run_intraday.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_run_round.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_run_snapshot.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_run_trials.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_run_view.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_session_columns.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_session_factor.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_sharadar_fundamentals.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_stage_split.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_themes.py +0 -0
- {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_vocabulary.py +0 -0
{quantlab_factor_agent-0.1.0/quantlab_factor_agent.egg-info → quantlab_factor_agent-0.2.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quantlab-factor-agent
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: LLM-driven, mechanism-first formulaic factor research on quantlab, evaluated with KunQuant.
|
|
5
5
|
Author: Zhaorong Dai
|
|
6
6
|
License-Expression: MIT AND Apache-2.0
|
|
@@ -148,14 +148,56 @@ print(report.markdown())
|
|
|
148
148
|
|
|
149
149
|
Point the config's data paths at your own quantlab stores first. A campaign runs for hours,
|
|
150
150
|
mostly waiting on the model, and needs no GPU; `Acceptance` uses CUDA when it is available.
|
|
151
|
-
|
|
151
|
+
Before a long run, `factor-agent check <config>` reports everything `run()` would refuse
|
|
152
|
+
(a store that does not exist, an unset API key variable, a resume that changed the
|
|
153
|
+
config) in seconds, without writing anything; `--ping` also sends each backend one short
|
|
154
|
+
request.
|
|
155
|
+
|
|
156
|
+
## Watch a run in the browser
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
factor-agent-web runs/ # then open http://127.0.0.1:8000
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The web panel lists your runs and opens each one: whether its accepted factors improve
|
|
163
|
+
your model, a card per factor, every trial on a Lineage graph and the admission map, the
|
|
164
|
+
LLM calls of the round in progress, the memory and the config. It only reads the run
|
|
165
|
+
directory, listens on localhost only, and on a server you reach it through an SSH tunnel.
|
|
166
|
+
See [the web frontend](https://github.com/ZhaorongDai/quantlab-factor-agent/blob/main/docs/user-guide/web.md).
|
|
167
|
+
|
|
168
|
+
<p align="center">
|
|
169
|
+
<img src="https://raw.githubusercontent.com/ZhaorongDai/quantlab-factor-agent/main/docs/_static/web/trials-lineage.png" alt="The trials page: a round's plan with its Picked parents outlined in blue, the Lineage graph of every trial by round with arrows from parent to child, and the admission map of partial-IC t against the largest correlation with an existing factor" width="100%">
|
|
170
|
+
</p>
|
|
171
|
+
|
|
172
|
+
## Building on earlier trials
|
|
173
|
+
|
|
174
|
+
Every trial after the first round either starts a new direction or builds on an earlier
|
|
175
|
+
one, its parent, as a refinement or a mutation. A trial's **Lineage** sums up the trials
|
|
176
|
+
built on it: how many, how many stopped early, which were accepted and how far the best
|
|
177
|
+
got. With `context.lineage` the planner and the proposer read it. With
|
|
178
|
+
`round.picked_parents: k` the code, not the planner, chooses the parents of k slots each
|
|
179
|
+
round, the **Picked parents**: it ranks the evaluated trials by quality, fertility and
|
|
180
|
+
novelty, each shared over the **Trial graph** of trials related by generation, code
|
|
181
|
+
structure and correlation, so a trial whose relatives did well rises with them. The
|
|
182
|
+
`trial_graph` section sets how much is shared and how the graph is built. Both are off by
|
|
183
|
+
default. The web panel's
|
|
184
|
+
"why these parents" shows, for each round, how propagation moved each trial's utilities
|
|
185
|
+
and rank:
|
|
186
|
+
|
|
187
|
+
<p align="center">
|
|
188
|
+
<img src="https://raw.githubusercontent.com/ZhaorongDai/quantlab-factor-agent/main/docs/_static/web/propagation.png" alt="The propagation view: per utility a trial's own value, its neighbours' average with the heaviest neighbours listed, and its value after propagation; beside it the rank ladder, where the trial rises from rank 15 to rank 10" width="100%">
|
|
189
|
+
</p>
|
|
190
|
+
|
|
191
|
+
See [building on earlier trials](https://github.com/ZhaorongDai/quantlab-factor-agent/blob/main/docs/user-guide/concepts.md#building-on-earlier-trials)
|
|
192
|
+
and [the campaign config](https://github.com/ZhaorongDai/quantlab-factor-agent/blob/main/docs/user-guide/campaign.md#settings-with-defaults).
|
|
152
193
|
|
|
153
194
|
## Documentation
|
|
154
195
|
|
|
155
196
|
- [How the agent mines factors](https://github.com/ZhaorongDai/quantlab-factor-agent/blob/main/docs/user-guide/mechanism.md): the search, the judges, the learning loop and what the models may see, in one diagram
|
|
156
197
|
- [First campaign](https://github.com/ZhaorongDai/quantlab-factor-agent/blob/main/docs/getting-started/quickstart.md): run one and read its outcome
|
|
157
198
|
- [Concepts](https://github.com/ZhaorongDai/quantlab-factor-agent/blob/main/docs/user-guide/concepts.md): roles, factor sets, segments, what "accepted" means
|
|
158
|
-
- [The campaign config](https://github.com/ZhaorongDai/quantlab-factor-agent/blob/main/docs/user-guide/campaign.md): every section, stopping and resuming
|
|
199
|
+
- [The campaign config](https://github.com/ZhaorongDai/quantlab-factor-agent/blob/main/docs/user-guide/campaign.md): every section, checking a config, stopping and resuming
|
|
200
|
+
- [The web frontend](https://github.com/ZhaorongDai/quantlab-factor-agent/blob/main/docs/user-guide/web.md): every page of the panel, and how to reach it on a server
|
|
159
201
|
- [The vocabulary](https://github.com/ZhaorongDai/quantlab-factor-agent/blob/main/docs/user-guide/vocabulary.md) and [prompts](https://github.com/ZhaorongDai/quantlab-factor-agent/blob/main/docs/user-guide/prompts.md): what candidates may read and write, and what the models are told
|
|
160
202
|
- [LLM backends](https://github.com/ZhaorongDai/quantlab-factor-agent/blob/main/docs/user-guide/llm-backends.md): providers, the LLM log, replay, cost
|
|
161
203
|
- [Gate, review and evaluation](https://github.com/ZhaorongDai/quantlab-factor-agent/blob/main/docs/user-guide/evaluation.md) and [acceptance](https://github.com/ZhaorongDai/quantlab-factor-agent/blob/main/docs/user-guide/acceptance.md): how a factor is judged
|
|
@@ -113,14 +113,56 @@ print(report.markdown())
|
|
|
113
113
|
|
|
114
114
|
Point the config's data paths at your own quantlab stores first. A campaign runs for hours,
|
|
115
115
|
mostly waiting on the model, and needs no GPU; `Acceptance` uses CUDA when it is available.
|
|
116
|
-
|
|
116
|
+
Before a long run, `factor-agent check <config>` reports everything `run()` would refuse
|
|
117
|
+
(a store that does not exist, an unset API key variable, a resume that changed the
|
|
118
|
+
config) in seconds, without writing anything; `--ping` also sends each backend one short
|
|
119
|
+
request.
|
|
120
|
+
|
|
121
|
+
## Watch a run in the browser
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
factor-agent-web runs/ # then open http://127.0.0.1:8000
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The web panel lists your runs and opens each one: whether its accepted factors improve
|
|
128
|
+
your model, a card per factor, every trial on a Lineage graph and the admission map, the
|
|
129
|
+
LLM calls of the round in progress, the memory and the config. It only reads the run
|
|
130
|
+
directory, listens on localhost only, and on a server you reach it through an SSH tunnel.
|
|
131
|
+
See [the web frontend](https://github.com/ZhaorongDai/quantlab-factor-agent/blob/main/docs/user-guide/web.md).
|
|
132
|
+
|
|
133
|
+
<p align="center">
|
|
134
|
+
<img src="https://raw.githubusercontent.com/ZhaorongDai/quantlab-factor-agent/main/docs/_static/web/trials-lineage.png" alt="The trials page: a round's plan with its Picked parents outlined in blue, the Lineage graph of every trial by round with arrows from parent to child, and the admission map of partial-IC t against the largest correlation with an existing factor" width="100%">
|
|
135
|
+
</p>
|
|
136
|
+
|
|
137
|
+
## Building on earlier trials
|
|
138
|
+
|
|
139
|
+
Every trial after the first round either starts a new direction or builds on an earlier
|
|
140
|
+
one, its parent, as a refinement or a mutation. A trial's **Lineage** sums up the trials
|
|
141
|
+
built on it: how many, how many stopped early, which were accepted and how far the best
|
|
142
|
+
got. With `context.lineage` the planner and the proposer read it. With
|
|
143
|
+
`round.picked_parents: k` the code, not the planner, chooses the parents of k slots each
|
|
144
|
+
round, the **Picked parents**: it ranks the evaluated trials by quality, fertility and
|
|
145
|
+
novelty, each shared over the **Trial graph** of trials related by generation, code
|
|
146
|
+
structure and correlation, so a trial whose relatives did well rises with them. The
|
|
147
|
+
`trial_graph` section sets how much is shared and how the graph is built. Both are off by
|
|
148
|
+
default. The web panel's
|
|
149
|
+
"why these parents" shows, for each round, how propagation moved each trial's utilities
|
|
150
|
+
and rank:
|
|
151
|
+
|
|
152
|
+
<p align="center">
|
|
153
|
+
<img src="https://raw.githubusercontent.com/ZhaorongDai/quantlab-factor-agent/main/docs/_static/web/propagation.png" alt="The propagation view: per utility a trial's own value, its neighbours' average with the heaviest neighbours listed, and its value after propagation; beside it the rank ladder, where the trial rises from rank 15 to rank 10" width="100%">
|
|
154
|
+
</p>
|
|
155
|
+
|
|
156
|
+
See [building on earlier trials](https://github.com/ZhaorongDai/quantlab-factor-agent/blob/main/docs/user-guide/concepts.md#building-on-earlier-trials)
|
|
157
|
+
and [the campaign config](https://github.com/ZhaorongDai/quantlab-factor-agent/blob/main/docs/user-guide/campaign.md#settings-with-defaults).
|
|
117
158
|
|
|
118
159
|
## Documentation
|
|
119
160
|
|
|
120
161
|
- [How the agent mines factors](https://github.com/ZhaorongDai/quantlab-factor-agent/blob/main/docs/user-guide/mechanism.md): the search, the judges, the learning loop and what the models may see, in one diagram
|
|
121
162
|
- [First campaign](https://github.com/ZhaorongDai/quantlab-factor-agent/blob/main/docs/getting-started/quickstart.md): run one and read its outcome
|
|
122
163
|
- [Concepts](https://github.com/ZhaorongDai/quantlab-factor-agent/blob/main/docs/user-guide/concepts.md): roles, factor sets, segments, what "accepted" means
|
|
123
|
-
- [The campaign config](https://github.com/ZhaorongDai/quantlab-factor-agent/blob/main/docs/user-guide/campaign.md): every section, stopping and resuming
|
|
164
|
+
- [The campaign config](https://github.com/ZhaorongDai/quantlab-factor-agent/blob/main/docs/user-guide/campaign.md): every section, checking a config, stopping and resuming
|
|
165
|
+
- [The web frontend](https://github.com/ZhaorongDai/quantlab-factor-agent/blob/main/docs/user-guide/web.md): every page of the panel, and how to reach it on a server
|
|
124
166
|
- [The vocabulary](https://github.com/ZhaorongDai/quantlab-factor-agent/blob/main/docs/user-guide/vocabulary.md) and [prompts](https://github.com/ZhaorongDai/quantlab-factor-agent/blob/main/docs/user-guide/prompts.md): what candidates may read and write, and what the models are told
|
|
125
167
|
- [LLM backends](https://github.com/ZhaorongDai/quantlab-factor-agent/blob/main/docs/user-guide/llm-backends.md): providers, the LLM log, replay, cost
|
|
126
168
|
- [Gate, review and evaluation](https://github.com/ZhaorongDai/quantlab-factor-agent/blob/main/docs/user-guide/evaluation.md) and [acceptance](https://github.com/ZhaorongDai/quantlab-factor-agent/blob/main/docs/user-guide/acceptance.md): how a factor is judged
|
|
@@ -56,6 +56,11 @@ def direction(ic_mean: float) -> int:
|
|
|
56
56
|
return int(np.sign(ic_mean)) if math.isfinite(ic_mean) else 0
|
|
57
57
|
|
|
58
58
|
|
|
59
|
+
def directional_t(metrics: dict) -> float:
|
|
60
|
+
"""An evaluated trial's residual (partial IC) Newey-West t in its direction, from its record's ``metrics``."""
|
|
61
|
+
return direction(metrics["ic_mean"]) * metrics["partial_ic_t"]
|
|
62
|
+
|
|
63
|
+
|
|
59
64
|
def monotonicity(decile_returns, sign: int) -> float:
|
|
60
65
|
"""The Spearman correlation of the decile number with the decile returns, signed by ``sign``."""
|
|
61
66
|
returns = np.asarray(decile_returns, dtype=float)
|
|
@@ -5,7 +5,8 @@ Newey-West t, its residual (partial) IC, what the pool explains of it and the
|
|
|
5
5
|
pool factors most correlated with it, the order of its deciles' returns (as
|
|
6
6
|
ranks), the nearest existing factor and the correlation with it, the nearest
|
|
7
7
|
known factor and its sampled correlation, the rejection reasons, and each
|
|
8
|
-
accepted factor's marginal weight and residual IC in the accepted pool
|
|
8
|
+
accepted factor's marginal weight and residual IC in the accepted pool, and,
|
|
9
|
+
when given, each accepted factor's and each parent's Lineage (``_lineage``). It
|
|
9
10
|
never shows a return, a date, or advice to change windows or parameters. The first round
|
|
10
11
|
has no history; it sees the data, the seed directions and the existing
|
|
11
12
|
factor families (the reference set's, then the known factors', marked as
|
|
@@ -17,12 +18,16 @@ research direction names the trial it builds on (``parent``).
|
|
|
17
18
|
|
|
18
19
|
import math
|
|
19
20
|
import re
|
|
21
|
+
from typing import TYPE_CHECKING
|
|
20
22
|
|
|
21
|
-
from factor_agent._admission import direction, reason_kind
|
|
23
|
+
from factor_agent._admission import direction, directional_t, reason_kind
|
|
22
24
|
from factor_agent.evaluator import Contribution
|
|
23
25
|
from factor_agent.ledger import TrialRecord
|
|
24
26
|
from factor_agent.zoo import owner
|
|
25
27
|
|
|
28
|
+
if TYPE_CHECKING: # _lineage imports ``signed`` from here, so its Lineage is imported for the annotations only
|
|
29
|
+
from factor_agent._lineage import Lineage
|
|
30
|
+
|
|
26
31
|
_DECIMAL = re.compile(r"[-+]?\d+\.\d+")
|
|
27
32
|
_TRIAL_ID = re.compile(r"T(\d+)-(\d+)")
|
|
28
33
|
|
|
@@ -103,8 +108,8 @@ def history(
|
|
|
103
108
|
if statistics and r.evaluated:
|
|
104
109
|
m = r.metrics
|
|
105
110
|
line += (
|
|
106
|
-
f"; training IC {
|
|
107
|
-
f"residual IC {
|
|
111
|
+
f"; training IC {signed(m['ic_mean'], 4)} (t {signed(m['ic_t'], 2)}), "
|
|
112
|
+
f"residual IC {signed(m['partial_ic_mean'], 4)} (t {signed(m['partial_ic_t'], 2)})"
|
|
108
113
|
)
|
|
109
114
|
share = m.get("pool_share", float("nan"))
|
|
110
115
|
if share is not None and math.isfinite(share) and share > 0:
|
|
@@ -162,7 +167,7 @@ def theme_groups(names: list[str], matrix, threshold: float) -> list[list[str]]:
|
|
|
162
167
|
opens a group of its own otherwise. The groups are in the order they
|
|
163
168
|
were opened, each in the order its trials were tried.
|
|
164
169
|
"""
|
|
165
|
-
order = sorted(range(len(names)), key=lambda k:
|
|
170
|
+
order = sorted(range(len(names)), key=lambda k: trial_order(names[k]))
|
|
166
171
|
group_of: dict[int, int] = {}
|
|
167
172
|
groups: list[list[str]] = []
|
|
168
173
|
for position, j in enumerate(order):
|
|
@@ -177,7 +182,7 @@ def theme_groups(names: list[str], matrix, threshold: float) -> list[list[str]]:
|
|
|
177
182
|
return groups
|
|
178
183
|
|
|
179
184
|
|
|
180
|
-
def
|
|
185
|
+
def trial_order(trial: str) -> tuple[int, int]:
|
|
181
186
|
"""The round and the slot of a trial id (``trial_id``)."""
|
|
182
187
|
number, slot = _TRIAL_ID.fullmatch(trial).groups()
|
|
183
188
|
return int(number), int(slot)
|
|
@@ -217,7 +222,7 @@ def themes(
|
|
|
217
222
|
first = records[0]
|
|
218
223
|
accepted = [t for t in group if by_id[t].status == "accepted"]
|
|
219
224
|
best_t, best_id = max(
|
|
220
|
-
((
|
|
225
|
+
((directional_t(r.metrics), t) for t, r in zip(group, records)
|
|
221
226
|
if math.isfinite(r.metrics["partial_ic_t"])),
|
|
222
227
|
default=(float("nan"), ""),
|
|
223
228
|
)
|
|
@@ -230,7 +235,7 @@ def themes(
|
|
|
230
235
|
f"({', '.join(group)}), {len(accepted)} accepted{' (' + ', '.join(accepted) + ')' if accepted else ''}"
|
|
231
236
|
)
|
|
232
237
|
if best_id:
|
|
233
|
-
line += f"; largest residual t in its direction {
|
|
238
|
+
line += f"; largest residual t in its direction {signed(best_t, 2)} ({best_id})"
|
|
234
239
|
line += f"; last tried in round {max(r.round for r in records)}"
|
|
235
240
|
if refused:
|
|
236
241
|
line += "; refused: " + ", ".join(f"{label} {count}" for label, count in refused.items())
|
|
@@ -267,8 +272,11 @@ def last_round(trials: list[tuple[str, TrialRecord]], earlier: list[tuple[str, T
|
|
|
267
272
|
return "\n".join(blocks)
|
|
268
273
|
|
|
269
274
|
|
|
270
|
-
def parent(trial: str, r: TrialRecord, earlier: list[tuple[str, TrialRecord]] = ()) -> str:
|
|
271
|
-
"""The trial a research direction builds on, as its proposer reads it: what it computed, its code, statistics and reasons.
|
|
275
|
+
def parent(trial: str, r: TrialRecord, earlier: list[tuple[str, TrialRecord]] = (), lineage: "Lineage | None" = None) -> str:
|
|
276
|
+
"""The trial a research direction builds on, as its proposer reads it: what it computed, its code, statistics and reasons.
|
|
277
|
+
|
|
278
|
+
With its ``lineage`` the block ends with how the trials built on it did.
|
|
279
|
+
"""
|
|
272
280
|
code = "\n".join(" " + line for line in r.candidate.get("code", "").splitlines())
|
|
273
281
|
lines = [
|
|
274
282
|
f"{trial}, round {r.round}, {r.candidate.get('mechanism_type', '?')}: {r.candidate.get('description', '')}",
|
|
@@ -276,7 +284,7 @@ def parent(trial: str, r: TrialRecord, earlier: list[tuple[str, TrialRecord]] =
|
|
|
276
284
|
f" outcome: {outcome(r)}",
|
|
277
285
|
" code:", code,
|
|
278
286
|
]
|
|
279
|
-
return "\n".join(lines + _statistics(r, earlier))
|
|
287
|
+
return "\n".join(lines + _statistics(r, earlier) + ([f" {lineage.text()}"] if lineage is not None else []))
|
|
280
288
|
|
|
281
289
|
|
|
282
290
|
def _statistics(r: TrialRecord, earlier: list[tuple[str, TrialRecord]] = ()) -> list[str]:
|
|
@@ -288,17 +296,17 @@ def _statistics(r: TrialRecord, earlier: list[tuple[str, TrialRecord]] = ()) ->
|
|
|
288
296
|
if r.evaluated:
|
|
289
297
|
m = r.metrics
|
|
290
298
|
nearest = (
|
|
291
|
-
f"nearest existing factor {m['nearest']}, correlation {
|
|
299
|
+
f"nearest existing factor {m['nearest']}, correlation {signed(m['nearest_correlation'], 2)}"
|
|
292
300
|
if m["nearest"] else "no existing factor to compare with"
|
|
293
301
|
)
|
|
294
302
|
if m["nearest_known"]:
|
|
295
303
|
nearest += (
|
|
296
304
|
f"; nearest known factor {m['nearest_known']}, "
|
|
297
|
-
f"sampled correlation {
|
|
305
|
+
f"sampled correlation {signed(m['nearest_known_correlation'], 2)}"
|
|
298
306
|
)
|
|
299
307
|
lines.append(
|
|
300
|
-
f" training IC {
|
|
301
|
-
f"residual IC {
|
|
308
|
+
f" training IC {signed(m['ic_mean'], 4)} (Newey-West t {signed(m['ic_t'], 2)}); "
|
|
309
|
+
f"residual IC {signed(m['partial_ic_mean'], 4)} (Newey-West t {signed(m['partial_ic_t'], 2)}); {nearest}"
|
|
302
310
|
)
|
|
303
311
|
overlap = _overlap(m, {accepted_label(e): t for t, e in earlier if e.status == "accepted"})
|
|
304
312
|
if overlap:
|
|
@@ -324,8 +332,13 @@ def accepted(records: list[TrialRecord]) -> str:
|
|
|
324
332
|
)
|
|
325
333
|
|
|
326
334
|
|
|
327
|
-
def best(
|
|
328
|
-
|
|
335
|
+
def best(
|
|
336
|
+
trials: list[tuple[str, TrialRecord]], contributions: dict[str, Contribution], lineage: "dict[str, Lineage] | None" = None,
|
|
337
|
+
) -> str:
|
|
338
|
+
"""The accepted factors (of ``named`` trials): id, name, mechanism, code, and marginal weight and residual IC in the accepted pool.
|
|
339
|
+
|
|
340
|
+
With ``lineage`` (by trial id, ``_lineage.lineages``) each also carries its Lineage.
|
|
341
|
+
"""
|
|
329
342
|
blocks = []
|
|
330
343
|
for trial, r in trials:
|
|
331
344
|
if r.status != "accepted":
|
|
@@ -339,8 +352,10 @@ def best(trials: list[tuple[str, TrialRecord]], contributions: dict[str, Contrib
|
|
|
339
352
|
]
|
|
340
353
|
if part is not None:
|
|
341
354
|
lines.append(
|
|
342
|
-
f" in the accepted pool: marginal weight {
|
|
355
|
+
f" in the accepted pool: marginal weight {signed(part.weight, 3)}, residual IC {signed(part.residual_ic, 4)}"
|
|
343
356
|
)
|
|
357
|
+
if lineage and trial in lineage:
|
|
358
|
+
lines.append(f" {lineage[trial].text()}")
|
|
344
359
|
blocks.append("\n".join(lines))
|
|
345
360
|
return "\n".join(blocks)
|
|
346
361
|
|
|
@@ -370,7 +385,7 @@ def _overlap(metrics: dict, ids: dict[str, str]) -> str:
|
|
|
370
385
|
share, overlaps = metrics.get("pool_share"), metrics.get("overlaps") or []
|
|
371
386
|
if share is None or not math.isfinite(share) or not overlaps:
|
|
372
387
|
return ""
|
|
373
|
-
named = ", ".join(f"{ids.get(name, name)} {
|
|
388
|
+
named = ", ".join(f"{ids.get(name, name)} {signed(correlation, 2)}" for name, correlation in overlaps)
|
|
374
389
|
return (
|
|
375
390
|
f" {max(share, 0):.0%} of its daily cross-section is explained by the reference set and the accepted "
|
|
376
391
|
f"factors (the part the residual IC leaves out); most correlated: {named}"
|
|
@@ -386,7 +401,7 @@ def _direction(r: TrialRecord, by_id: dict[str, TrialRecord] | None = None) -> s
|
|
|
386
401
|
earlier = (by_id or {}).get(d["parent"])
|
|
387
402
|
if earlier is None or not earlier.evaluated:
|
|
388
403
|
return f"{text} of {d['parent']}"
|
|
389
|
-
return f"{text} of {d['parent']} (whose residual t was {
|
|
404
|
+
return f"{text} of {d['parent']} (whose residual t was {signed(earlier.metrics['partial_ic_t'], 2)})"
|
|
390
405
|
|
|
391
406
|
|
|
392
407
|
def outcome(r: TrialRecord) -> str:
|
|
@@ -395,5 +410,6 @@ def outcome(r: TrialRecord) -> str:
|
|
|
395
410
|
|
|
396
411
|
|
|
397
412
|
|
|
398
|
-
def
|
|
413
|
+
def signed(value: float, digits: int) -> str:
|
|
414
|
+
"""``value`` with its sign and ``digits`` decimals, or ``n/a`` when it is not a number."""
|
|
399
415
|
return f"{value:+.{digits}f}" if math.isfinite(value) else "n/a"
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Each trial's Lineage: the trials that built on it, and how they did.
|
|
2
|
+
|
|
3
|
+
A trial's children are the trials whose direction names it as ``parent``
|
|
4
|
+
(direct children only). Its Lineage counts them by action (refinement or
|
|
5
|
+
mutation), those that stopped before evaluation (refused by the parser,
|
|
6
|
+
the Gate or the reviewer, or not compiled) and those accepted, and keeps
|
|
7
|
+
the best evaluated child with its residual t in its direction, beside the
|
|
8
|
+
trial's own. The prompts carry it as one line (``Lineage.text``); the
|
|
9
|
+
fertility is the same counts as one number, for choosing parents.
|
|
10
|
+
|
|
11
|
+
A child's outcome is 0 when it stopped before evaluation and otherwise the
|
|
12
|
+
amount its residual t in its direction exceeds the admission threshold it
|
|
13
|
+
was judged against (``_admission.t_threshold`` at the evaluated-trial
|
|
14
|
+
count after its round: admission judges every candidate of a round at that
|
|
15
|
+
count, whatever its place in the round), floored at 0. The fertility is the
|
|
16
|
+
mean of the ``fertile_children`` (``context.fertile_children``, 3 by
|
|
17
|
+
default) best outcomes, padded with zeros, so a parent is rewarded for
|
|
18
|
+
children that pass, not for many attempts.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import math
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
|
|
24
|
+
from factor_agent._admission import directional_t, t_threshold
|
|
25
|
+
from factor_agent._feedback import signed
|
|
26
|
+
from factor_agent.config import AdmissionConfig
|
|
27
|
+
from factor_agent.ledger import TrialRecord
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class Lineage:
|
|
31
|
+
"""The trials built on one trial; see the module docstring."""
|
|
32
|
+
|
|
33
|
+
attempts: int = 0
|
|
34
|
+
refinements: int = 0
|
|
35
|
+
mutations: int = 0
|
|
36
|
+
#: Children that stopped before evaluation.
|
|
37
|
+
unevaluated: int = 0
|
|
38
|
+
#: The accepted children's ids, in the order they were tried.
|
|
39
|
+
accepted: tuple[str, ...] = ()
|
|
40
|
+
#: The evaluated child with the largest residual t in its direction, "" when none was evaluated.
|
|
41
|
+
best_child: str = ""
|
|
42
|
+
best_child_t: float = float("nan")
|
|
43
|
+
#: The trial's own residual t in its direction, NaN when it was not evaluated.
|
|
44
|
+
own_t: float = float("nan")
|
|
45
|
+
fertility: float = 0.0
|
|
46
|
+
|
|
47
|
+
def text(self) -> str:
|
|
48
|
+
"""The line the planner's accepted factors and the proposer's parent block carry."""
|
|
49
|
+
if not self.attempts:
|
|
50
|
+
return "lineage: no trial has built on it yet"
|
|
51
|
+
line = (
|
|
52
|
+
f"lineage: {_count(self.attempts, 'trial')} built on it ({_count(self.refinements, 'refinement')}, "
|
|
53
|
+
f"{_count(self.mutations, 'mutation')}), {self.unevaluated} stopped before evaluation, "
|
|
54
|
+
+ (f"{len(self.accepted)} accepted ({', '.join(self.accepted)})" if self.accepted else "none accepted")
|
|
55
|
+
)
|
|
56
|
+
if not self.best_child:
|
|
57
|
+
return line + "; no child was evaluated"
|
|
58
|
+
return (
|
|
59
|
+
f"{line}; best evaluated child {self.best_child}, residual t in its direction {signed(self.best_child_t, 2)} "
|
|
60
|
+
f"against this trial's {signed(self.own_t, 2)}"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def lineages(
|
|
65
|
+
trials: list[tuple[str, TrialRecord]], admission: AdmissionConfig, fertile_children: int,
|
|
66
|
+
) -> dict[str, Lineage]:
|
|
67
|
+
"""The Lineage of every trial of ``trials`` (``_feedback.named``, in the ledger's order), by id.
|
|
68
|
+
|
|
69
|
+
``fertile_children`` is the count of best child outcomes the fertility is the mean of.
|
|
70
|
+
"""
|
|
71
|
+
evaluated_through: dict[int, int] = {}
|
|
72
|
+
for _, r in trials:
|
|
73
|
+
evaluated_through[r.round] = evaluated_through.get(r.round, 0) + r.evaluated
|
|
74
|
+
count = 0
|
|
75
|
+
for number in sorted(evaluated_through):
|
|
76
|
+
count += evaluated_through[number]
|
|
77
|
+
evaluated_through[number] = count
|
|
78
|
+
children: dict[str, list[tuple[str, TrialRecord]]] = {}
|
|
79
|
+
for trial, r in trials:
|
|
80
|
+
parent = (r.direction or {}).get("parent")
|
|
81
|
+
if parent:
|
|
82
|
+
children.setdefault(parent, []).append((trial, r))
|
|
83
|
+
result = {}
|
|
84
|
+
for trial, r in trials:
|
|
85
|
+
built = children.get(trial, [])
|
|
86
|
+
actions = [(c.direction or {}).get("action") for _, c in built]
|
|
87
|
+
scored = [(child, directional_t(c.metrics), c) for child, c in built if c.evaluated]
|
|
88
|
+
best = max(((t, child) for child, t, _ in scored if math.isfinite(t)), default=(float("nan"), ""))
|
|
89
|
+
outcomes = sorted(
|
|
90
|
+
(max(t - t_threshold(admission, evaluated_through[c.round]), 0.0) if math.isfinite(t) else 0.0
|
|
91
|
+
for _, t, c in scored),
|
|
92
|
+
reverse=True,
|
|
93
|
+
)
|
|
94
|
+
result[trial] = Lineage(
|
|
95
|
+
attempts=len(built), refinements=actions.count("refinement"), mutations=actions.count("mutation"),
|
|
96
|
+
unevaluated=len(built) - len(scored), accepted=tuple(child for child, c in built if c.status == "accepted"),
|
|
97
|
+
best_child=best[1], best_child_t=best[0], own_t=directional_t(r.metrics) if r.evaluated else float("nan"),
|
|
98
|
+
fertility=sum(outcomes[:fertile_children]) / fertile_children,
|
|
99
|
+
)
|
|
100
|
+
return result
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _count(n: int, noun: str) -> str:
|
|
104
|
+
"""``n`` of ``noun``, the noun in the plural unless ``n`` is 1."""
|
|
105
|
+
return f"{n} {noun}" + ("" if n == 1 else "s")
|