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.
Files changed (135) hide show
  1. {quantlab_factor_agent-0.1.0/quantlab_factor_agent.egg-info → quantlab_factor_agent-0.2.0}/PKG-INFO +45 -3
  2. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/README.md +44 -2
  3. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/__init__.py +1 -1
  4. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/_admission.py +5 -0
  5. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/_feedback.py +37 -21
  6. quantlab_factor_agent-0.2.0/factor_agent/_lineage.py +105 -0
  7. quantlab_factor_agent-0.2.0/factor_agent/_trial_graph.py +386 -0
  8. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/campaign.py +361 -44
  9. quantlab_factor_agent-0.2.0/factor_agent/cli.py +48 -0
  10. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/config.py +191 -14
  11. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/events.py +9 -2
  12. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/llm/anthropic.py +4 -1
  13. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/llm/base.py +49 -16
  14. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/llm/claude_code.py +4 -1
  15. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/llm/openai_compatible.py +4 -2
  16. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/llm/replay.py +3 -0
  17. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/llm/scripted.py +3 -0
  18. quantlab_factor_agent-0.2.0/factor_agent/preflight.py +94 -0
  19. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/prompts.py +27 -6
  20. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/researcher.py +38 -11
  21. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/snapshot.py +53 -27
  22. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/web/_ledger.py +12 -0
  23. quantlab_factor_agent-0.2.0/factor_agent/web/_plans.py +268 -0
  24. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/web/_round.py +8 -3
  25. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/web/app.py +13 -1
  26. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/web/run_view.py +17 -3
  27. quantlab_factor_agent-0.2.0/factor_agent/web/static/assets/index-C1Tg3uN3.css +1 -0
  28. quantlab_factor_agent-0.2.0/factor_agent/web/static/assets/index-Dvvznupv.js +74 -0
  29. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/web/static/index.html +2 -2
  30. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/pyproject.toml +2 -1
  31. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0/quantlab_factor_agent.egg-info}/PKG-INFO +45 -3
  32. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/quantlab_factor_agent.egg-info/SOURCES.txt +12 -2
  33. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/quantlab_factor_agent.egg-info/entry_points.txt +1 -0
  34. quantlab_factor_agent-0.2.0/tests/test_campaign_check.py +318 -0
  35. quantlab_factor_agent-0.2.0/tests/test_campaign_lineage.py +338 -0
  36. quantlab_factor_agent-0.2.0/tests/test_campaign_picked_parents.py +263 -0
  37. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_config.py +116 -1
  38. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_gate.py +3 -2
  39. quantlab_factor_agent-0.2.0/tests/test_import.py +7 -0
  40. quantlab_factor_agent-0.2.0/tests/test_lineage.py +89 -0
  41. quantlab_factor_agent-0.2.0/tests/test_run_lineage.py +172 -0
  42. quantlab_factor_agent-0.2.0/tests/test_trial_graph.py +259 -0
  43. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_web.py +74 -2
  44. quantlab_factor_agent-0.1.0/factor_agent/web/static/assets/index-9QjjJgl3.css +0 -1
  45. quantlab_factor_agent-0.1.0/factor_agent/web/static/assets/index-iOE9ei_S.js +0 -74
  46. quantlab_factor_agent-0.1.0/tests/test_campaign_lineage.py +0 -179
  47. quantlab_factor_agent-0.1.0/tests/test_import.py +0 -5
  48. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/LICENSE +0 -0
  49. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/KUNQUANT_LICENSE +0 -0
  50. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/_acceptance_model.py +0 -0
  51. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/_expression_fold.py +0 -0
  52. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/_max_drawdown.py +0 -0
  53. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/_model_cache.py +0 -0
  54. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/_scheduler.py +0 -0
  55. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/acceptance.py +0 -0
  56. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/accepted/__init__.py +0 -0
  57. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/aggregations.py +0 -0
  58. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/candidate.py +0 -0
  59. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/derived.py +0 -0
  60. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/evaluator.py +0 -0
  61. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/exported.py +0 -0
  62. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/gate/__init__.py +0 -0
  63. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/gate/_canonical.py +0 -0
  64. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/gate/_checks.py +0 -0
  65. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/gate/_constraints.py +0 -0
  66. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/gate/_dimension.py +0 -0
  67. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/gate/_originality.py +0 -0
  68. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/gate/_sandbox.py +0 -0
  69. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/gate/graph.py +0 -0
  70. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/gate/stages.py +0 -0
  71. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/gate/vocabulary.py +0 -0
  72. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/ledger.py +0 -0
  73. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/llm/__init__.py +0 -0
  74. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/llm/_common.py +0 -0
  75. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/llm/_raw.py +0 -0
  76. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/llm/log.py +0 -0
  77. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/period_lag.py +0 -0
  78. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/scoring.py +0 -0
  79. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/segments.py +0 -0
  80. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/session.py +0 -0
  81. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/web/__init__.py +0 -0
  82. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/web/_acceptance.py +0 -0
  83. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/web/_config.py +0 -0
  84. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/web/_diagnosis.py +0 -0
  85. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/web/_files.py +0 -0
  86. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/web/_health.py +0 -0
  87. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/web/_ic.py +0 -0
  88. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/web/_memory.py +0 -0
  89. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/web/_results.py +0 -0
  90. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/web/_trials.py +0 -0
  91. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/factor_agent/zoo.py +0 -0
  92. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/quantlab_factor_agent.egg-info/dependency_links.txt +0 -0
  93. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/quantlab_factor_agent.egg-info/requires.txt +0 -0
  94. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/quantlab_factor_agent.egg-info/top_level.txt +0 -0
  95. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/setup.cfg +0 -0
  96. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_acceptance.py +0 -0
  97. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_admission_threshold.py +0 -0
  98. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_campaign.py +0 -0
  99. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_campaign_loop.py +0 -0
  100. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_campaign_prompts.py +0 -0
  101. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_confirmation.py +0 -0
  102. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_derived_columns.py +0 -0
  103. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_evaluator.py +0 -0
  104. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_events.py +0 -0
  105. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_expression_fold.py +0 -0
  106. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_high_frequency_family.py +0 -0
  107. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_intraday_acceptance.py +0 -0
  108. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_intraday_example.py +0 -0
  109. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_intraday_family.py +0 -0
  110. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_intraday_fields.py +0 -0
  111. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_intraday_gate.py +0 -0
  112. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_intraday_prompt.py +0 -0
  113. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_intraday_vocabulary.py +0 -0
  114. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_intraday_windows.py +0 -0
  115. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_llm_backends.py +0 -0
  116. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_max_drawdown.py +0 -0
  117. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_period_lag.py +0 -0
  118. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_proposal_groups.py +0 -0
  119. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_run_acceptance.py +0 -0
  120. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_run_config.py +0 -0
  121. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_run_diagnosis.py +0 -0
  122. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_run_factors.py +0 -0
  123. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_run_health.py +0 -0
  124. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_run_ic.py +0 -0
  125. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_run_intraday.py +0 -0
  126. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_run_round.py +0 -0
  127. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_run_snapshot.py +0 -0
  128. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_run_trials.py +0 -0
  129. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_run_view.py +0 -0
  130. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_session_columns.py +0 -0
  131. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_session_factor.py +0 -0
  132. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_sharadar_fundamentals.py +0 -0
  133. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_stage_split.py +0 -0
  134. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_themes.py +0 -0
  135. {quantlab_factor_agent-0.1.0 → quantlab_factor_agent-0.2.0}/tests/test_vocabulary.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quantlab-factor-agent
3
- Version: 0.1.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
- Watch a run in the browser with `factor-agent-web` ([web frontend](https://github.com/ZhaorongDai/quantlab-factor-agent/blob/main/docs/user-guide/web.md)).
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
- Watch a run in the browser with `factor-agent-web` ([web frontend](https://github.com/ZhaorongDai/quantlab-factor-agent/blob/main/docs/user-guide/web.md)).
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
@@ -2,7 +2,7 @@
2
2
 
3
3
  from factor_agent import _expression_fold, _max_drawdown
4
4
 
5
- __version__ = "0.1.0"
5
+ __version__ = "0.2.0"
6
6
 
7
7
  _expression_fold.patch()
8
8
  _max_drawdown.patch()
@@ -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. It
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 {_num(m['ic_mean'], 4)} (t {_num(m['ic_t'], 2)}), "
107
- f"residual IC {_num(m['partial_ic_mean'], 4)} (t {_num(m['partial_ic_t'], 2)})"
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: _trial_order(names[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 _trial_order(trial: str) -> tuple[int, int]:
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
- ((direction(r.metrics["ic_mean"]) * r.metrics["partial_ic_t"], t) for t, r in zip(group, records)
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 {_num(best_t, 2)} ({best_id})"
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 {_num(m['nearest_correlation'], 2)}"
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 {_num(m['nearest_known_correlation'], 2)}"
305
+ f"sampled correlation {signed(m['nearest_known_correlation'], 2)}"
298
306
  )
299
307
  lines.append(
300
- f" training IC {_num(m['ic_mean'], 4)} (Newey-West t {_num(m['ic_t'], 2)}); "
301
- f"residual IC {_num(m['partial_ic_mean'], 4)} (Newey-West t {_num(m['partial_ic_t'], 2)}); {nearest}"
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(trials: list[tuple[str, TrialRecord]], contributions: dict[str, Contribution]) -> str:
328
- """The accepted factors (of ``named`` trials): id, name, mechanism, code, and marginal weight and residual IC in the accepted pool."""
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 {_num(part.weight, 3)}, residual IC {_num(part.residual_ic, 4)}"
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)} {_num(correlation, 2)}" for name, correlation in overlaps)
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 {_num(earlier.metrics['partial_ic_t'], 2)})"
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 _num(value: float, digits: int) -> str:
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")