quantlab-factor-agent 0.1.0__py3-none-any.whl
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.
- factor_agent/KUNQUANT_LICENSE +201 -0
- factor_agent/__init__.py +8 -0
- factor_agent/_acceptance_model.py +93 -0
- factor_agent/_admission.py +168 -0
- factor_agent/_expression_fold.py +48 -0
- factor_agent/_feedback.py +399 -0
- factor_agent/_max_drawdown.py +51 -0
- factor_agent/_model_cache.py +100 -0
- factor_agent/_scheduler.py +35 -0
- factor_agent/acceptance.py +808 -0
- factor_agent/accepted/__init__.py +1 -0
- factor_agent/aggregations.py +122 -0
- factor_agent/campaign.py +797 -0
- factor_agent/candidate.py +51 -0
- factor_agent/config.py +651 -0
- factor_agent/derived.py +98 -0
- factor_agent/evaluator.py +1096 -0
- factor_agent/events.py +343 -0
- factor_agent/exported.py +177 -0
- factor_agent/gate/__init__.py +359 -0
- factor_agent/gate/_canonical.py +331 -0
- factor_agent/gate/_checks.py +85 -0
- factor_agent/gate/_constraints.py +85 -0
- factor_agent/gate/_dimension.py +177 -0
- factor_agent/gate/_originality.py +146 -0
- factor_agent/gate/_sandbox.py +105 -0
- factor_agent/gate/graph.py +223 -0
- factor_agent/gate/stages.py +383 -0
- factor_agent/gate/vocabulary.py +1097 -0
- factor_agent/ledger.py +85 -0
- factor_agent/llm/__init__.py +0 -0
- factor_agent/llm/_common.py +18 -0
- factor_agent/llm/_raw.py +78 -0
- factor_agent/llm/anthropic.py +100 -0
- factor_agent/llm/base.py +124 -0
- factor_agent/llm/claude_code.py +101 -0
- factor_agent/llm/log.py +74 -0
- factor_agent/llm/openai_compatible.py +113 -0
- factor_agent/llm/replay.py +59 -0
- factor_agent/llm/scripted.py +39 -0
- factor_agent/period_lag.py +91 -0
- factor_agent/prompts.py +557 -0
- factor_agent/researcher.py +615 -0
- factor_agent/scoring.py +143 -0
- factor_agent/segments.py +184 -0
- factor_agent/session.py +500 -0
- factor_agent/snapshot.py +219 -0
- factor_agent/web/__init__.py +7 -0
- factor_agent/web/_acceptance.py +145 -0
- factor_agent/web/_config.py +51 -0
- factor_agent/web/_diagnosis.py +110 -0
- factor_agent/web/_files.py +78 -0
- factor_agent/web/_health.py +207 -0
- factor_agent/web/_ic.py +27 -0
- factor_agent/web/_ledger.py +293 -0
- factor_agent/web/_memory.py +42 -0
- factor_agent/web/_results.py +77 -0
- factor_agent/web/_round.py +219 -0
- factor_agent/web/_trials.py +112 -0
- factor_agent/web/app.py +118 -0
- factor_agent/web/run_view.py +329 -0
- factor_agent/web/static/assets/index-9QjjJgl3.css +1 -0
- factor_agent/web/static/assets/index-iOE9ei_S.js +74 -0
- factor_agent/web/static/index.html +13 -0
- factor_agent/zoo.py +136 -0
- quantlab_factor_agent-0.1.0.dist-info/METADATA +172 -0
- quantlab_factor_agent-0.1.0.dist-info/RECORD +72 -0
- quantlab_factor_agent-0.1.0.dist-info/WHEEL +5 -0
- quantlab_factor_agent-0.1.0.dist-info/entry_points.txt +2 -0
- quantlab_factor_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
- quantlab_factor_agent-0.1.0.dist-info/licenses/factor_agent/KUNQUANT_LICENSE +201 -0
- quantlab_factor_agent-0.1.0.dist-info/top_level.txt +1 -0
factor_agent/config.py
ADDED
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
"""The campaign config: one YAML file holds every setting of a run.
|
|
2
|
+
|
|
3
|
+
``data``, ``reference`` and ``known`` hold quantlab components: each is a
|
|
4
|
+
``get_config()`` dict inline, the path of a store folder whose
|
|
5
|
+
``component.json`` describes it (``quantlab.core.store_folder.save_component``),
|
|
6
|
+
or ``{store: <folder>, factor_names: [...]}``, that store's factor pinned to
|
|
7
|
+
some of its outputs. ``reference`` lists the reference set: the factors
|
|
8
|
+
candidates must be original against (the ``Zoo``) and, in admission,
|
|
9
|
+
uncorrelated with and incremental to; a factor's ``factor_names`` selects
|
|
10
|
+
its columns. ``known`` lists the known factors: every other existing factor
|
|
11
|
+
a candidate is checked against for novelty (the ``Zoo``), never one it is
|
|
12
|
+
measured incremental to; an output also in ``reference`` is taken there
|
|
13
|
+
only (``CampaignConfig.load_factors``). A malformed entry of either list is
|
|
14
|
+
refused when the config is read; a store or class is only opened when the
|
|
15
|
+
factors are loaded.
|
|
16
|
+
|
|
17
|
+
``fields`` (field files, one per input family) and ``operators`` (the
|
|
18
|
+
operator file) make the ``Vocabulary`` candidates write with; the
|
|
19
|
+
framework has none of its own. ``operator_modules`` lists the modules,
|
|
20
|
+
besides KunQuant and ``quantlab.factor``, an operator may come from.
|
|
21
|
+
|
|
22
|
+
``market`` describes the data in the prompts, ``mechanisms`` lists the
|
|
23
|
+
mechanism types candidates belong to (by default ``DEFAULT_MECHANISMS``),
|
|
24
|
+
each with a one-line description, and ``prompts`` maps a template's name
|
|
25
|
+
(see ``factor_agent.prompts.TEMPLATES``) to the file that replaces it.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import dataclasses
|
|
29
|
+
|
|
30
|
+
from dataclasses import dataclass, field
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from typing import Any
|
|
33
|
+
|
|
34
|
+
import yaml
|
|
35
|
+
from quantlab.core.component import rebuild
|
|
36
|
+
from quantlab.core.store_folder import load_component
|
|
37
|
+
|
|
38
|
+
from factor_agent import _acceptance_model
|
|
39
|
+
from factor_agent.gate.vocabulary import Vocabulary, VocabularyError
|
|
40
|
+
from factor_agent.prompts import Templates
|
|
41
|
+
from factor_agent.segments import PURGE_BARS, Segment, Segments
|
|
42
|
+
from factor_agent.snapshot import SearchFiles
|
|
43
|
+
from factor_agent.zoo import owner
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class DataConfig:
|
|
49
|
+
"""The data the Evaluator reads.
|
|
50
|
+
|
|
51
|
+
``prices`` is the whole-market price dataset, ``label`` the forward
|
|
52
|
+
label (masked by day-t membership when there is a membership) and
|
|
53
|
+
``membership`` the members of each day (an ``IndexConstituentDataset``)
|
|
54
|
+
or None, when every symbol is scored. ``intraday`` is the whole-market
|
|
55
|
+
intraday dataset an intraday family's fields read (one-minute trade
|
|
56
|
+
bars, say), or None without an intraday family.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
prices: dict | str
|
|
60
|
+
label: dict | str
|
|
61
|
+
membership: dict | str | None = None
|
|
62
|
+
intraday: dict | str | None = None
|
|
63
|
+
|
|
64
|
+
def load(self, name: str) -> Any:
|
|
65
|
+
"""Rebuild the component ``name``; a list of components is rebuilt item by item."""
|
|
66
|
+
return load_each(getattr(self, name))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class NeutralizeConfig:
|
|
71
|
+
"""What ranked factors are neutralised against.
|
|
72
|
+
|
|
73
|
+
``exposures`` is the dataset, or list of datasets, as components, that
|
|
74
|
+
holds ``size_column`` (the market cap; its logarithm is regressed on) and
|
|
75
|
+
``industry_column`` (the integer industry code).
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
exposures: dict | str | list
|
|
79
|
+
size_column: str
|
|
80
|
+
industry_column: str
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(frozen=True)
|
|
84
|
+
class ScoringConfig:
|
|
85
|
+
"""How a factor's raw values become the values that are judged (``factor_agent.scoring``).
|
|
86
|
+
|
|
87
|
+
``neutralize`` is what ranked factors are neutralised against, or None
|
|
88
|
+
to score them as ranked. It must be given, as null to turn
|
|
89
|
+
neutralisation off, so no campaign is scored one way by default.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
neutralize: NeutralizeConfig | None
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def from_dict(cls, raw: dict) -> "ScoringConfig":
|
|
96
|
+
"""Read the ``scoring`` section.
|
|
97
|
+
|
|
98
|
+
Raises
|
|
99
|
+
------
|
|
100
|
+
TypeError
|
|
101
|
+
If the section holds another key than ``neutralize``, or ``neutralize`` another key than its fields.
|
|
102
|
+
KeyError
|
|
103
|
+
If ``neutralize`` is missing.
|
|
104
|
+
"""
|
|
105
|
+
unknown = set(raw) - {"neutralize"}
|
|
106
|
+
if unknown:
|
|
107
|
+
raise TypeError(f"campaign config: unknown scoring key(s) {sorted(unknown)}")
|
|
108
|
+
if "neutralize" not in raw:
|
|
109
|
+
raise KeyError("campaign config: scoring needs neutralize (null to score the ranked values)")
|
|
110
|
+
neutralize = raw["neutralize"]
|
|
111
|
+
return cls(neutralize=None if neutralize is None else NeutralizeConfig(**neutralize))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def load_each(value: dict | str | list) -> Any:
|
|
115
|
+
"""Rebuild a component (see ``load``), or each of a list of components."""
|
|
116
|
+
return [load(item) for item in value] if isinstance(value, list) else load(value)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def load(component: dict | str) -> Any:
|
|
120
|
+
"""Rebuild a component given as a ``get_config()`` dict, a store folder path or a store entry.
|
|
121
|
+
|
|
122
|
+
Raises
|
|
123
|
+
------
|
|
124
|
+
ValueError
|
|
125
|
+
If a store entry names an output its store does not have.
|
|
126
|
+
TypeError
|
|
127
|
+
If a store entry holds a key other than ``store`` and ``factor_names``, or names
|
|
128
|
+
outputs of a store whose component has no ``factor_names`` (a dataset).
|
|
129
|
+
"""
|
|
130
|
+
if isinstance(component, str):
|
|
131
|
+
return load_component(component)
|
|
132
|
+
if "store" not in component:
|
|
133
|
+
return rebuild(component)
|
|
134
|
+
unknown = set(component) - {"store", "factor_names"}
|
|
135
|
+
if unknown:
|
|
136
|
+
raise TypeError(f"store entry {component['store']}: unknown key(s) {sorted(unknown)}")
|
|
137
|
+
stored = load_component(component["store"])
|
|
138
|
+
names = component.get("factor_names")
|
|
139
|
+
if names is None:
|
|
140
|
+
return stored
|
|
141
|
+
missing = [name for name in names if name not in stored.get_factor_names()]
|
|
142
|
+
if missing:
|
|
143
|
+
raise ValueError(f"store entry {component['store']}: no output(s) {missing}")
|
|
144
|
+
return type(stored)(dataclasses.replace(stored.config, factor_names=tuple(names)))
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass(frozen=True)
|
|
148
|
+
class EvaluatorConfig:
|
|
149
|
+
"""How the Evaluator computes the round's graph."""
|
|
150
|
+
|
|
151
|
+
#: Bars read before the segment start so every window is filled.
|
|
152
|
+
warmup_bars: int = 260
|
|
153
|
+
#: KunQuant executor threads.
|
|
154
|
+
njobs: int = 51
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@dataclass(frozen=True)
|
|
158
|
+
class GateConfig:
|
|
159
|
+
"""The Gate's sandbox limits and graph constraints."""
|
|
160
|
+
|
|
161
|
+
#: Seconds the sandboxed graph build may take, counted from when the sandbox is ready.
|
|
162
|
+
timeout_seconds: float = 10.0
|
|
163
|
+
#: Memory, in MB, the sandboxed graph build may use.
|
|
164
|
+
memory_mb: int = 1024
|
|
165
|
+
#: Nodes a candidate's graph may have.
|
|
166
|
+
max_nodes: int = 200
|
|
167
|
+
#: Hand-set constants (other than 0 and +-1) per operator.
|
|
168
|
+
max_free_parameter_ratio: float = 0.5
|
|
169
|
+
#: Distinct input fields (open, high, low, close, volume, vwap, amount) a candidate may read.
|
|
170
|
+
max_base_fields: int = 5
|
|
171
|
+
#: Nodes of the largest subtree a candidate may share with a zoo factor.
|
|
172
|
+
max_common_subtree_nodes: int = 5
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@dataclass(frozen=True)
|
|
176
|
+
class AdmissionConfig:
|
|
177
|
+
"""What an evaluated candidate needs to be accepted."""
|
|
178
|
+
|
|
179
|
+
#: Newey-West t of the partial IC the first evaluated trial needs; the
|
|
180
|
+
#: threshold rises with the trial count (Bonferroni, see ``_admission``).
|
|
181
|
+
t_threshold: float = 3.0
|
|
182
|
+
#: Largest absolute cross-sectional correlation with a reference or accepted factor.
|
|
183
|
+
max_correlation: float = 0.7
|
|
184
|
+
#: Smallest Spearman correlation of the decile returns with the decile number.
|
|
185
|
+
min_monotonicity: float = 0.6
|
|
186
|
+
#: Smallest mean daily share of the members on which the candidate has a value.
|
|
187
|
+
min_coverage: float = 0.5
|
|
188
|
+
#: Power of the trial count the anchor's p-value is divided by: 1 is Bonferroni, a smaller
|
|
189
|
+
#: one lets the threshold rise more slowly (0.5 counts 100 trials as 10).
|
|
190
|
+
trial_exponent: float = 1.0
|
|
191
|
+
#: Newey-West t of the partial IC, in the factor's direction, a candidate needs on the
|
|
192
|
+
#: confirmation segment (``segments.confirmation``), whose statistics no prompt shows; read
|
|
193
|
+
#: only when there is one.
|
|
194
|
+
confirm_t: float = 2.0
|
|
195
|
+
|
|
196
|
+
def __post_init__(self):
|
|
197
|
+
if isinstance(self.confirm_t, bool) or not isinstance(self.confirm_t, (int, float)):
|
|
198
|
+
raise TypeError(f"admission: confirm_t is {self.confirm_t!r}; it is a number")
|
|
199
|
+
if not 0 < self.trial_exponent <= 1:
|
|
200
|
+
raise ValueError(f"admission: trial_exponent is {self.trial_exponent}; it lies in (0, 1]")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@dataclass(frozen=True)
|
|
204
|
+
class RoundConfig:
|
|
205
|
+
"""The size of a round, the planner's quotas and when the campaign stops."""
|
|
206
|
+
|
|
207
|
+
#: Candidates asked for per round.
|
|
208
|
+
candidates: int = 20
|
|
209
|
+
#: Candidates of every mechanism type per round; the rest are allocated by Thompson sampling.
|
|
210
|
+
min_per_type: int = 3
|
|
211
|
+
#: Times a candidate the parser or the Gate refused is sent back to the proposer to fix.
|
|
212
|
+
retries: int = 2
|
|
213
|
+
#: Rounds the campaign runs at most.
|
|
214
|
+
max_rounds: int = 30
|
|
215
|
+
#: Trials (candidates, failed ones included) the campaign runs at most; a round that
|
|
216
|
+
#: would go past it is not started.
|
|
217
|
+
max_trials: int = 600
|
|
218
|
+
#: Consecutive rounds without an acceptance after which the campaign stops.
|
|
219
|
+
patience: int = 5
|
|
220
|
+
#: Rounds between two distillations of the ledger into the memory.
|
|
221
|
+
memory_every: int = 5
|
|
222
|
+
#: Seed of the Thompson sampling; round n draws from ``(seed, n)``.
|
|
223
|
+
seed: int = 0
|
|
224
|
+
#: Proposal groups a round's directions are split into, each written, fixed and reviewed by
|
|
225
|
+
#: calls of its own that run side by side; a resume may change it.
|
|
226
|
+
groups: int = 1
|
|
227
|
+
|
|
228
|
+
def __post_init__(self):
|
|
229
|
+
if self.groups < 1:
|
|
230
|
+
raise ValueError(f"round: groups is {self.groups}; a round needs at least one proposal group")
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@dataclass(frozen=True)
|
|
234
|
+
class ContextConfig:
|
|
235
|
+
"""What the prompt context holds; the run snapshot records it at the first start, and a resume may not change it."""
|
|
236
|
+
|
|
237
|
+
#: Rounds whose trials the planner reads one line each (the recent history); older trials are
|
|
238
|
+
#: counted by mechanism type and outcome (the earlier summary).
|
|
239
|
+
recent_rounds: int = 5
|
|
240
|
+
#: Whether the planner and the proposer read the earlier trials (the memory, the history, the
|
|
241
|
+
#: last round and the accepted factors) and a memory is distilled. Off, each round is planned
|
|
242
|
+
#: and written from the data and the seed directions alone, every direction new: the control
|
|
243
|
+
#: of a test of whether the search learns from its trials.
|
|
244
|
+
feedback: bool = True
|
|
245
|
+
#: The correlation, in absolute value, at which an evaluated trial is grouped with an earlier one
|
|
246
|
+
#: (``_feedback.theme_groups``), the groups the planner reads; None (the default) groups none.
|
|
247
|
+
theme_correlation: float | None = None
|
|
248
|
+
#: Whether a new direction's candidate as correlated with an earlier trial is sent back to be
|
|
249
|
+
#: rewritten once before it is evaluated; it needs ``theme_correlation``.
|
|
250
|
+
send_back_repeats: bool = False
|
|
251
|
+
|
|
252
|
+
def __post_init__(self):
|
|
253
|
+
if self.recent_rounds < 1:
|
|
254
|
+
raise ValueError(f"context: recent_rounds is {self.recent_rounds}; at least the last round is listed")
|
|
255
|
+
if not isinstance(self.feedback, bool):
|
|
256
|
+
raise TypeError(f"context: feedback is {self.feedback!r}; it is true or false")
|
|
257
|
+
if self.theme_correlation is not None:
|
|
258
|
+
if isinstance(self.theme_correlation, bool) or not isinstance(self.theme_correlation, (int, float)):
|
|
259
|
+
raise TypeError(f"context: theme_correlation is {self.theme_correlation!r}; it is a number or null")
|
|
260
|
+
if not 0 < self.theme_correlation < 1:
|
|
261
|
+
raise ValueError(f"context: theme_correlation is {self.theme_correlation}; it lies in (0, 1)")
|
|
262
|
+
if not self.feedback:
|
|
263
|
+
raise ValueError("context: theme_correlation needs feedback, since the groups are made of earlier trials")
|
|
264
|
+
if not isinstance(self.send_back_repeats, bool):
|
|
265
|
+
raise TypeError(f"context: send_back_repeats is {self.send_back_repeats!r}; it is true or false")
|
|
266
|
+
if self.send_back_repeats and self.theme_correlation is None:
|
|
267
|
+
raise ValueError("context: send_back_repeats needs theme_correlation, the correlation at which a trial repeats one")
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
#: The values of ``acceptance.contribution``.
|
|
271
|
+
CONTRIBUTIONS = ("none", "drop_one", "add_one", "both")
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@dataclass(frozen=True)
|
|
275
|
+
class AcceptanceConfig:
|
|
276
|
+
"""How ``Acceptance`` validates, opens the hold-out and exports.
|
|
277
|
+
|
|
278
|
+
``model`` is the acceptance model, a template (``factor_agent._acceptance_model``):
|
|
279
|
+
the component config of a quantlab ``BaseModel`` or ``ModelEnsemble``,
|
|
280
|
+
whose own factors are the baseline (empty: the reference set). Every
|
|
281
|
+
baseline feature, and each accepted factor's scored values, is read as
|
|
282
|
+
the campaign's ``scoring`` reads.
|
|
283
|
+
|
|
284
|
+
Raises
|
|
285
|
+
------
|
|
286
|
+
ValueError
|
|
287
|
+
If there is no seed, ``model`` is a ``SeedEnsemble``, or ``contribution`` is not one of ``CONTRIBUTIONS``.
|
|
288
|
+
TypeError
|
|
289
|
+
If ``model`` is not a model's component config.
|
|
290
|
+
"""
|
|
291
|
+
|
|
292
|
+
#: The acceptance model's template; required, so no campaign is accepted against a default model.
|
|
293
|
+
model: dict
|
|
294
|
+
#: The seeds every model is trained with; each replaces every member's ``random_seed``.
|
|
295
|
+
seeds: tuple[int, ...] = (0, 1, 2)
|
|
296
|
+
#: Share of the training window each model validates (early-stops) on, at its end.
|
|
297
|
+
val_size: float = 0.2
|
|
298
|
+
#: Members held by the plain TopK book.
|
|
299
|
+
top_k: int = 100
|
|
300
|
+
#: Each accepted factor's contribution in validation: none, drop_one, add_one or both; never on the hold-out.
|
|
301
|
+
contribution: str = "none"
|
|
302
|
+
#: Where exported stores go, ``<factor_root>/<name>/``; an export needs it.
|
|
303
|
+
factor_root: str | None = None
|
|
304
|
+
#: The package the exported ``FactorKunQuant`` subclasses are written into, one module each.
|
|
305
|
+
package: str = "factor_agent.accepted"
|
|
306
|
+
#: First date of an exported store; None: the training segment's start.
|
|
307
|
+
export_start: str | None = None
|
|
308
|
+
#: Last date of an exported store; None: the hold-out's end, else the validation's.
|
|
309
|
+
export_end: str | None = None
|
|
310
|
+
|
|
311
|
+
def __post_init__(self):
|
|
312
|
+
object.__setattr__(self, "seeds", tuple(self.seeds))
|
|
313
|
+
if not self.seeds:
|
|
314
|
+
raise ValueError("acceptance: at least one seed is needed")
|
|
315
|
+
if self.contribution not in CONTRIBUTIONS:
|
|
316
|
+
raise ValueError(f"acceptance.contribution {self.contribution!r} is not one of {list(CONTRIBUTIONS)}")
|
|
317
|
+
_acceptance_model.check(self.model)
|
|
318
|
+
|
|
319
|
+
@classmethod
|
|
320
|
+
def from_dict(cls, raw: dict | None) -> "AcceptanceConfig":
|
|
321
|
+
"""Read the ``acceptance`` section.
|
|
322
|
+
|
|
323
|
+
Raises
|
|
324
|
+
------
|
|
325
|
+
KeyError
|
|
326
|
+
If the section, or its ``model``, is missing.
|
|
327
|
+
TypeError
|
|
328
|
+
If it holds ``hyperparameters`` or ``features``, which the model template now holds.
|
|
329
|
+
"""
|
|
330
|
+
raw = dict(raw or {})
|
|
331
|
+
for removed, now in (("hyperparameters", "hyperparameters"), ("features", "factors")):
|
|
332
|
+
if removed in raw:
|
|
333
|
+
raise TypeError(f"campaign config: acceptance.{removed} is gone; give them as acceptance.model's {now}")
|
|
334
|
+
if "model" not in raw:
|
|
335
|
+
raise KeyError(
|
|
336
|
+
"campaign config: acceptance.model is needed, the component config of the quantlab model "
|
|
337
|
+
"(or ModelEnsemble) the accepted factors are tested in"
|
|
338
|
+
)
|
|
339
|
+
return cls(**raw)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
@dataclass(frozen=True)
|
|
343
|
+
class RoleConfig:
|
|
344
|
+
"""One role's LLM: the backend name, the model and the backend's own settings."""
|
|
345
|
+
|
|
346
|
+
backend: str
|
|
347
|
+
model: str
|
|
348
|
+
options: dict
|
|
349
|
+
|
|
350
|
+
@classmethod
|
|
351
|
+
def from_dict(cls, data: dict) -> "RoleConfig":
|
|
352
|
+
"""Split a role's YAML mapping into the backend, the model and the rest."""
|
|
353
|
+
data = dict(data)
|
|
354
|
+
return cls(backend=data.pop("backend"), model=data.pop("model"), options=data)
|
|
355
|
+
|
|
356
|
+
def spec(self) -> dict:
|
|
357
|
+
"""The dict ``make_backend`` takes."""
|
|
358
|
+
return {"backend": self.backend, "model": self.model, **self.options}
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
#: The LLM roles of a round, each configured under ``llm``.
|
|
362
|
+
ROLES = ("planner", "proposer", "reviewer", "distiller")
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
@dataclass(frozen=True)
|
|
366
|
+
class Mechanism:
|
|
367
|
+
"""A mechanism type a candidate's hypothesis belongs to, with the line the prompts describe it by."""
|
|
368
|
+
|
|
369
|
+
name: str
|
|
370
|
+
description: str
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
#: The mechanism types of a config that names none.
|
|
374
|
+
DEFAULT_MECHANISMS = (
|
|
375
|
+
Mechanism("behavioural_bias", "investors' systematic errors (over- and underreaction, anchoring, limited "
|
|
376
|
+
"attention) that prices correct later"),
|
|
377
|
+
Mechanism("risk_compensation", "a return paid for bearing a risk investors demand to be compensated for"),
|
|
378
|
+
Mechanism("liquidity", "the cost and the pressure of trading: illiquidity premia, liquidity demand and its "
|
|
379
|
+
"reversal"),
|
|
380
|
+
Mechanism("microstructure", "the mechanics of trading within and between bars: order flow, the bid-ask "
|
|
381
|
+
"bounce, the open and the close"),
|
|
382
|
+
Mechanism("information_diffusion", "news that reaches prices gradually, across investors, related firms "
|
|
383
|
+
"or time"),
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
@dataclass(frozen=True)
|
|
388
|
+
class CampaignConfig:
|
|
389
|
+
"""Every setting of a campaign, read from one YAML file with ``from_file``.
|
|
390
|
+
|
|
391
|
+
``roles`` maps each of ``ROLES`` to its LLM; the roles may use
|
|
392
|
+
different backends, so the reviewer can be another provider's model.
|
|
393
|
+
``directions`` are the seed research directions the planner is shown.
|
|
394
|
+
``market`` describes the data in the prompts, ``mechanisms`` are the
|
|
395
|
+
mechanism types in plan order, and ``prompts`` maps a template's name to
|
|
396
|
+
the file that replaces it; the replacing files are read and checked for
|
|
397
|
+
their placeholders when the config is made.
|
|
398
|
+
|
|
399
|
+
Raises
|
|
400
|
+
------
|
|
401
|
+
PromptError
|
|
402
|
+
Naming every problem of the replaced templates.
|
|
403
|
+
ValueError
|
|
404
|
+
If the mechanism types repeat or a round cannot hold its quota of them.
|
|
405
|
+
"""
|
|
406
|
+
|
|
407
|
+
run_dir: str
|
|
408
|
+
segments: Segments
|
|
409
|
+
data: DataConfig
|
|
410
|
+
scoring: ScoringConfig
|
|
411
|
+
roles: dict
|
|
412
|
+
#: The field files, one per input family candidates may read; see ``Vocabulary``.
|
|
413
|
+
fields: tuple
|
|
414
|
+
#: The operator file: the operators candidates may call and their parameter values.
|
|
415
|
+
operators: str
|
|
416
|
+
acceptance: AcceptanceConfig
|
|
417
|
+
#: The data as the prompts describe it, such as "adjusted daily bars of every US common stock".
|
|
418
|
+
market: str
|
|
419
|
+
#: Modules besides KunQuant and ``quantlab.factor`` an operator's class or callable may come from.
|
|
420
|
+
operator_modules: tuple = ()
|
|
421
|
+
#: The reference factors, as components; see the module docstring.
|
|
422
|
+
reference: tuple = ()
|
|
423
|
+
#: The known factors besides the reference set, as components; see the module docstring.
|
|
424
|
+
known: tuple = ()
|
|
425
|
+
directions: tuple = ()
|
|
426
|
+
evaluator: EvaluatorConfig = EvaluatorConfig()
|
|
427
|
+
gate: GateConfig = GateConfig()
|
|
428
|
+
admission: AdmissionConfig = AdmissionConfig()
|
|
429
|
+
round: RoundConfig = RoundConfig()
|
|
430
|
+
context: ContextConfig = ContextConfig()
|
|
431
|
+
mechanisms: tuple = DEFAULT_MECHANISMS
|
|
432
|
+
prompts: dict = field(default_factory=dict)
|
|
433
|
+
#: The YAML text ``from_file`` read, which a run keeps for each opening; None for a config made in code.
|
|
434
|
+
source: str | None = field(default=None, compare=False, repr=False)
|
|
435
|
+
|
|
436
|
+
def __post_init__(self):
|
|
437
|
+
object.__setattr__(self, "mechanisms", tuple(self.mechanisms))
|
|
438
|
+
Templates.load(self.prompts) # every problem of the replaced templates, before anything runs
|
|
439
|
+
names = self.mechanism_types
|
|
440
|
+
if not names or len(set(names)) != len(names):
|
|
441
|
+
raise ValueError(f"campaign config: the mechanism types {list(names)} must be given and distinct")
|
|
442
|
+
if self.round.candidates < self.round.min_per_type * len(names):
|
|
443
|
+
raise ValueError(
|
|
444
|
+
f"round: {self.round.candidates} candidates cannot hold {self.round.min_per_type} of each of "
|
|
445
|
+
f"{len(names)} mechanism types"
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
def load_factors(self) -> tuple[list, list]:
|
|
449
|
+
"""Rebuild the reference factors and the known factors, ``(reference, known)``, each in config order.
|
|
450
|
+
|
|
451
|
+
An output of a known factor is dropped when a reference factor shows
|
|
452
|
+
the same output of the same class (through ``zoo.owner``, so a
|
|
453
|
+
neutralised store counts as the factor it neutralises); the factor
|
|
454
|
+
keeps its other outputs, and one left with none is dropped.
|
|
455
|
+
|
|
456
|
+
Raises
|
|
457
|
+
------
|
|
458
|
+
ValueError, TypeError
|
|
459
|
+
As ``load`` does, for an entry its store or class does not fit.
|
|
460
|
+
"""
|
|
461
|
+
reference = [load(component) for component in self.reference]
|
|
462
|
+
taken = {key for factor in reference for key in _output_keys(factor)}
|
|
463
|
+
known = []
|
|
464
|
+
for factor in (load(component) for component in self.known):
|
|
465
|
+
names = tuple(name for name, key in zip(factor.get_factor_names(), _output_keys(factor)) if key not in taken)
|
|
466
|
+
if names == tuple(factor.get_factor_names()):
|
|
467
|
+
known.append(factor)
|
|
468
|
+
elif names:
|
|
469
|
+
known.append(type(factor)(dataclasses.replace(factor.config, factor_names=names)))
|
|
470
|
+
return reference, known
|
|
471
|
+
|
|
472
|
+
@property
|
|
473
|
+
def mechanism_types(self) -> tuple[str, ...]:
|
|
474
|
+
"""The names of the mechanism types, in plan order."""
|
|
475
|
+
return tuple(m.name for m in self.mechanisms)
|
|
476
|
+
|
|
477
|
+
@property
|
|
478
|
+
def training(self) -> Segment:
|
|
479
|
+
"""The segment the campaign evaluates."""
|
|
480
|
+
return self.segments.training
|
|
481
|
+
|
|
482
|
+
def search_files(self) -> SearchFiles:
|
|
483
|
+
"""The files the run searches with: the field files, the operator file and the replaced templates."""
|
|
484
|
+
return SearchFiles(self.fields, self.operators, self.prompts)
|
|
485
|
+
|
|
486
|
+
def templates(self, files: SearchFiles | None = None) -> Templates:
|
|
487
|
+
"""The prompt templates: the built-in ones, those ``files`` replace read from their files.
|
|
488
|
+
|
|
489
|
+
``files`` are the config's own by default, or a run snapshot's copies.
|
|
490
|
+
|
|
491
|
+
Raises
|
|
492
|
+
------
|
|
493
|
+
PromptError
|
|
494
|
+
With every problem of the replacing files.
|
|
495
|
+
"""
|
|
496
|
+
return Templates.load((files or self.search_files()).prompts)
|
|
497
|
+
|
|
498
|
+
def vocabulary(self, columns=None, files: SearchFiles | None = None, intraday_columns=None) -> Vocabulary:
|
|
499
|
+
"""The Vocabulary of the field and operator files; ``columns``, given, are the price dataset's.
|
|
500
|
+
|
|
501
|
+
``files`` are the config's own by default, or a run snapshot's copies;
|
|
502
|
+
``intraday_columns``, given, are the intraday dataset's, the session
|
|
503
|
+
inputs included.
|
|
504
|
+
|
|
505
|
+
Raises
|
|
506
|
+
------
|
|
507
|
+
VocabularyError
|
|
508
|
+
With every problem of the files, a field column missing from ``columns`` or
|
|
509
|
+
``intraday_columns`` included, and an intraday family without ``data.intraday``.
|
|
510
|
+
"""
|
|
511
|
+
files = files or self.search_files()
|
|
512
|
+
vocabulary = Vocabulary.load(files.fields, files.operators, self.operator_modules, columns, intraday_columns)
|
|
513
|
+
if vocabulary.intraday_family is not None and self.data.intraday is None:
|
|
514
|
+
raise VocabularyError([
|
|
515
|
+
f"{vocabulary.intraday_family} is an intraday family, but the config names no data.intraday, "
|
|
516
|
+
f"the dataset of its bars"
|
|
517
|
+
])
|
|
518
|
+
return vocabulary
|
|
519
|
+
|
|
520
|
+
@classmethod
|
|
521
|
+
def from_file(cls, path: str | Path) -> "CampaignConfig":
|
|
522
|
+
"""Read a campaign config from YAML; the paths of ``fields``, ``operators`` and ``prompts`` are relative to its folder.
|
|
523
|
+
|
|
524
|
+
Raises
|
|
525
|
+
------
|
|
526
|
+
TypeError
|
|
527
|
+
If a section holds a key its config class does not have, or names a budget or per-token prices.
|
|
528
|
+
KeyError
|
|
529
|
+
If a required section or role is missing.
|
|
530
|
+
ValueError
|
|
531
|
+
If a section's values do not fit together.
|
|
532
|
+
PromptError
|
|
533
|
+
Naming every problem of the replaced templates.
|
|
534
|
+
"""
|
|
535
|
+
source = Path(path).read_text()
|
|
536
|
+
raw = yaml.safe_load(source)
|
|
537
|
+
folder = Path(path).parent
|
|
538
|
+
sections = {
|
|
539
|
+
"evaluator": EvaluatorConfig, "gate": GateConfig,
|
|
540
|
+
"admission": AdmissionConfig, "round": RoundConfig, "context": ContextConfig,
|
|
541
|
+
}
|
|
542
|
+
unknown = set(raw) - {
|
|
543
|
+
"run_dir", "segments", "data", "scoring", "fields", "operators", "operator_modules", "reference", "known",
|
|
544
|
+
"directions", "llm", "market", "mechanisms", "prompts", "acceptance", *sections,
|
|
545
|
+
}
|
|
546
|
+
if unknown:
|
|
547
|
+
raise TypeError(f"campaign config: unknown section(s) {sorted(unknown)}")
|
|
548
|
+
unknown_roles = set(raw["llm"]) - set(ROLES)
|
|
549
|
+
if unknown_roles:
|
|
550
|
+
raise TypeError(f"campaign config: unknown LLM role(s) {sorted(unknown_roles)}; known: {list(ROLES)}")
|
|
551
|
+
_refuse_money(raw)
|
|
552
|
+
if "market" not in raw:
|
|
553
|
+
raise KeyError("campaign config: market is needed, the data as the prompts describe it")
|
|
554
|
+
optional = {}
|
|
555
|
+
if raw.get("mechanisms") is not None:
|
|
556
|
+
optional["mechanisms"] = tuple(Mechanism(**m) for m in raw["mechanisms"])
|
|
557
|
+
return cls(
|
|
558
|
+
run_dir=raw["run_dir"],
|
|
559
|
+
market=str(raw["market"]),
|
|
560
|
+
prompts={name: str(folder / file) for name, file in (raw.get("prompts") or {}).items()},
|
|
561
|
+
segments=_segments(raw["segments"]),
|
|
562
|
+
data=DataConfig(**raw["data"]),
|
|
563
|
+
scoring=ScoringConfig.from_dict(raw["scoring"]),
|
|
564
|
+
roles={role: RoleConfig.from_dict(raw["llm"][role]) for role in ROLES},
|
|
565
|
+
fields=tuple(str(folder / f) for f in raw["fields"]),
|
|
566
|
+
operators=str(folder / raw["operators"]),
|
|
567
|
+
operator_modules=tuple(raw.get("operator_modules") or ()),
|
|
568
|
+
reference=_entries("reference", raw.get("reference")),
|
|
569
|
+
known=_entries("known", raw.get("known")),
|
|
570
|
+
directions=tuple(raw.get("directions") or ()),
|
|
571
|
+
acceptance=AcceptanceConfig.from_dict(raw.get("acceptance")),
|
|
572
|
+
source=source,
|
|
573
|
+
**{name: klass(**raw[name]) for name, klass in sections.items() if name in raw},
|
|
574
|
+
**optional,
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _refuse_money(raw: dict) -> None:
|
|
579
|
+
"""Refuse a budget or per-token prices: spending limits belong to the provider.
|
|
580
|
+
|
|
581
|
+
Raises
|
|
582
|
+
------
|
|
583
|
+
TypeError
|
|
584
|
+
Naming the first such setting.
|
|
585
|
+
"""
|
|
586
|
+
if "budget_usd" in (raw.get("round") or {}):
|
|
587
|
+
raise TypeError("campaign config: round.budget_usd is gone; the provider's balance or account limit caps the spending")
|
|
588
|
+
for role, spec in raw["llm"].items():
|
|
589
|
+
for price in ("input_usd_per_mtok", "output_usd_per_mtok"):
|
|
590
|
+
if price in (spec or {}):
|
|
591
|
+
raise TypeError(f"campaign config: llm.{role}.{price} is gone; the library no longer counts the cost")
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def _output_keys(factor) -> list[tuple[str, str]]:
|
|
595
|
+
"""``(class of the owner, output name)`` of each output: what makes two outputs the same factor."""
|
|
596
|
+
kind = type(owner(factor)).__name__
|
|
597
|
+
return [(kind, name) for name in factor.get_factor_names()]
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def _entries(section: str, raw: list | None) -> tuple:
|
|
601
|
+
"""The component entries of ``section`` (``reference`` or ``known``), each checked for its form only.
|
|
602
|
+
|
|
603
|
+
Raises
|
|
604
|
+
------
|
|
605
|
+
TypeError
|
|
606
|
+
Naming every entry that is not a store folder path, a store entry
|
|
607
|
+
(``store`` and optionally a list of ``factor_names``) or a
|
|
608
|
+
``get_config()`` dict (one with a ``name``).
|
|
609
|
+
"""
|
|
610
|
+
entries = tuple(raw or ())
|
|
611
|
+
problems = []
|
|
612
|
+
for i, entry in enumerate(entries):
|
|
613
|
+
if isinstance(entry, str):
|
|
614
|
+
problem = None if entry else "an empty store folder path"
|
|
615
|
+
elif not isinstance(entry, dict):
|
|
616
|
+
problem = f"{entry!r} is neither a store folder, a store entry nor a component's config"
|
|
617
|
+
elif "store" in entry:
|
|
618
|
+
names = entry.get("factor_names", [])
|
|
619
|
+
if set(entry) - {"store", "factor_names"}:
|
|
620
|
+
problem = f"a store entry with the unknown key(s) {sorted(set(entry) - {'store', 'factor_names'})}"
|
|
621
|
+
elif not isinstance(entry["store"], str) or not entry["store"]:
|
|
622
|
+
problem = f"a store entry whose store {entry['store']!r} is not a folder path"
|
|
623
|
+
elif not isinstance(names, list) or not all(isinstance(name, str) for name in names):
|
|
624
|
+
problem = f"a store entry whose factor_names {names!r} is not a list of output names"
|
|
625
|
+
else:
|
|
626
|
+
problem = None
|
|
627
|
+
else:
|
|
628
|
+
problem = None if isinstance(entry.get("name"), str) else "a component's config without the name of its class"
|
|
629
|
+
if problem:
|
|
630
|
+
problems.append(f"{section}[{i}]: {problem}")
|
|
631
|
+
if problems:
|
|
632
|
+
raise TypeError("campaign config: " + "; ".join(problems))
|
|
633
|
+
return entries
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def _segments(raw: dict) -> Segments:
|
|
637
|
+
"""The ``segments`` section: ``training`` and, optionally, ``confirmation``, ``validation``, ``holdout`` and ``purge_bars``.
|
|
638
|
+
|
|
639
|
+
Raises
|
|
640
|
+
------
|
|
641
|
+
TypeError
|
|
642
|
+
If the section holds another key.
|
|
643
|
+
"""
|
|
644
|
+
unknown = set(raw) - {"training", "confirmation", "validation", "holdout", "purge_bars"}
|
|
645
|
+
if unknown:
|
|
646
|
+
raise TypeError(f"campaign config: unknown segment key(s) {sorted(unknown)}")
|
|
647
|
+
segments = {
|
|
648
|
+
name: Segment(name, str(raw[name]["start"]), str(raw[name]["end"]))
|
|
649
|
+
for name in ("training", "confirmation", "validation", "holdout") if name in raw
|
|
650
|
+
}
|
|
651
|
+
return Segments(**segments, purge_bars=int(raw.get("purge_bars", PURGE_BARS)))
|