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/prompts.py
ADDED
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
"""Prompt templates. They describe the data, the vocabulary and the training-segment feedback, never dates or returns.
|
|
2
|
+
|
|
3
|
+
Each role's prompt opens with the role, its task and who uses its answer,
|
|
4
|
+
holds the long inputs in the middle, each inside an XML tag (``<quota>``,
|
|
5
|
+
``<directions>``, ``<refused_candidates>``, ``<candidates>``, ``<previous_memory>``, ``<accepted>``, ``<history>``,
|
|
6
|
+
and the context sections under their keys), and ends by restating the task
|
|
7
|
+
and the shape of the answer. A fenced ``json`` block holds only that shape,
|
|
8
|
+
the last thing in the prompt, so no input reads like an answer to copy.
|
|
9
|
+
|
|
10
|
+
The templates are built in (``TEMPLATES``) and any of them may be replaced
|
|
11
|
+
from a file (``Templates.load``). A template names what it is filled with
|
|
12
|
+
as ``{placeholder}``; ``PLACEHOLDERS`` lists, per template, the ones it must
|
|
13
|
+
use and the ones it may. Every template may use ``market`` (the config's
|
|
14
|
+
description of the data) and ``horizon`` (the bars the label spans), and
|
|
15
|
+
every one but ``data``, which is the same in every round, ``round``.
|
|
16
|
+
``Prompts`` fills a template with those and with the data
|
|
17
|
+
section, itself the ``data`` template filled with the market, the
|
|
18
|
+
described inputs of the vocabulary, the scoring's sentence and the horizon.
|
|
19
|
+
When the vocabulary opens an intraday family, the code rules the proposer
|
|
20
|
+
is given end with ``INTRADAY`` (the two stages, their rules and
|
|
21
|
+
``INTRADAY_EXAMPLES``), and the vocabulary lists its operators by stage;
|
|
22
|
+
without one the prompts are those of a daily campaign.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import string
|
|
26
|
+
from collections.abc import Mapping
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
from factor_agent.gate import _dimension
|
|
31
|
+
from factor_agent.gate.vocabulary import Vocabulary
|
|
32
|
+
|
|
33
|
+
DATA = """\
|
|
34
|
+
The data: {market}.
|
|
35
|
+
{scoring}
|
|
36
|
+
The label a factor is scored against is the return over the next {horizon} bars.
|
|
37
|
+
Only a training period is ever evaluated; its dates are not disclosed.
|
|
38
|
+
|
|
39
|
+
The inputs, by family:
|
|
40
|
+
{inputs}"""
|
|
41
|
+
|
|
42
|
+
VOCABULARY = """\
|
|
43
|
+
Inputs: {inputs}
|
|
44
|
+
Operators:
|
|
45
|
+
{operators}
|
|
46
|
+
Parameter values (windows, lags and constants must be one of these; constants may be negative):
|
|
47
|
+
{parameters}
|
|
48
|
+
Builtins: {builtins}"""
|
|
49
|
+
|
|
50
|
+
CODE_RULES = """\
|
|
51
|
+
Write each factor as Python code over KunQuant operators. The code runs in a sandbox that builds a
|
|
52
|
+
KunQuant graph from it, so it uses only Python's own constructs and the names listed in <vocabulary>:
|
|
53
|
+
it may assign variables, define functions, and use for/while/if and `with Builder(loop):` blocks for
|
|
54
|
+
window loops. The sandbox refuses import, class, attribute access and file or network access, because
|
|
55
|
+
the graph is all that is evaluated. The code ends by assigning one operator to `alpha`, the factor's
|
|
56
|
+
value. Arithmetic operators (+, -, *, /) and comparisons work on operators; a comparison serves only
|
|
57
|
+
as the condition of a Select.
|
|
58
|
+
A factor's value should be comparable across the cross-section, so write it as a pure number: a
|
|
59
|
+
ratio, a return, a rank or a z-score. An output that carries a unit (a price, a dollar amount, a share
|
|
60
|
+
count) ranks the cross-section partly by that level, and that ranking is what the scoring compares,
|
|
61
|
+
unless the mechanism is about that level, as a price impact per dollar traded is.
|
|
62
|
+
Leave a missing value missing. The scoring skips a symbol without a value that day, so a missing
|
|
63
|
+
value needs no fill; a filled value is ranked as real data, so a symbol without the data is sorted as
|
|
64
|
+
if its value were the fill and the factor partly sorts the cross-section by whether the item is
|
|
65
|
+
reported; and coverage is measured before any fill, so a fill does not help a factor pass the
|
|
66
|
+
coverage rule. Fill (SetInfOrNanToValue) only when the mechanism says what a missing value means,
|
|
67
|
+
as an unreported R&D expense is zero, and state that reason in the description."""
|
|
68
|
+
|
|
69
|
+
#: What the code rules add when the vocabulary opens an intraday family: the two stages, their rules,
|
|
70
|
+
#: what the aggregations give, and ``INTRADAY_EXAMPLES``.
|
|
71
|
+
INTRADAY = """\
|
|
72
|
+
|
|
73
|
+
Intraday candidates. The fields of {family} are intraday: they hold a value for each bar of each
|
|
74
|
+
session (the bars of one regular trading day), not one per day. Code that reads them is written in two
|
|
75
|
+
stages, still as one piece of code ending in `alpha`:
|
|
76
|
+
- the intraday stage computes on each session's bars with the operators <vocabulary> allows in it, and
|
|
77
|
+
ends in session aggregations (SessionMean, SessionSum, ...), each reducing a session to one value per
|
|
78
|
+
symbol and day;
|
|
79
|
+
- the daily stage works on those values as on any daily field, with the daily operators, the daily
|
|
80
|
+
windows and the daily fields beside them.
|
|
81
|
+
The gate refuses a candidate that breaks one of these rules:
|
|
82
|
+
- an intraday field reaches the daily stage only through a session aggregation;
|
|
83
|
+
- the intraday stage reads no daily field, so a daily value enters only after an aggregation;
|
|
84
|
+
- an aggregation's inputs are intraday: no aggregation of a daily value or of another aggregation;
|
|
85
|
+
- each operator is used only in the stages <vocabulary> lists it under, so exponential averages, whose
|
|
86
|
+
memory has no end, are daily only;
|
|
87
|
+
- a window in the intraday stage counts bars and is one of the intraday windows ({windows}), a
|
|
88
|
+
lag one of the intraday lags ({lags}); in the daily stage either counts days and is one of the
|
|
89
|
+
daily values.
|
|
90
|
+
An intraday window stays within its session: it is missing on a session's first bars until it fits, so
|
|
91
|
+
it never reads the session before. A cross-sectional operator in the intraday stage ranks across the
|
|
92
|
+
market at each bar. A price field is missing on a bar without a trade, and a volume or trade count is 0
|
|
93
|
+
there. An aggregation uses the bars where its input has a value: SessionFirst and SessionLast take the
|
|
94
|
+
first and last value present, and a session without one gives a missing value, except SessionCount and
|
|
95
|
+
SessionSum, which give 0.
|
|
96
|
+
{examples}"""
|
|
97
|
+
|
|
98
|
+
#: Complete example intraday candidates, each a description and its code; they pass the Gate on the
|
|
99
|
+
#: example minute-bar field file (``configs/fields/massive_minute_bars.yaml``) and operator file.
|
|
100
|
+
INTRADAY_EXAMPLES = (
|
|
101
|
+
(
|
|
102
|
+
"the share of the session's volume traded in its last 60 bars, averaged over 20 days",
|
|
103
|
+
"late = SessionSum(Select(session_bars_left < 60, bar_volume, ConstantOp(0)))\n"
|
|
104
|
+
"alpha = WindowedAvg(late / SessionSum(bar_volume), 20)",
|
|
105
|
+
),
|
|
106
|
+
(
|
|
107
|
+
"the session's volatility of 5-bar returns, averaged over 5 days, against the 20-day volatility of daily returns",
|
|
108
|
+
"intraday_volatility = SessionStd(last_price / BackRef(last_price, 5) - 1)\n"
|
|
109
|
+
"daily_volatility = WindowedStddev(close / BackRef(close, 1) - 1, 20)\n"
|
|
110
|
+
"alpha = WindowedAvg(intraday_volatility, 5) / daily_volatility",
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
FIELDS = """\
|
|
115
|
+
- "hypothesis": the economic mechanism that makes the factor predict the return, in one or two sentences
|
|
116
|
+
- "mechanism_type": the mechanism type of its research direction
|
|
117
|
+
- "horizon_rationale": why the effect plays out over about {horizon} bars
|
|
118
|
+
- "description": what the code computes, in words
|
|
119
|
+
- "code": the Python code, ending with the assignment to `alpha`"""
|
|
120
|
+
|
|
121
|
+
#: The shape of one candidate in the proposer's answer (braces doubled for ``str.format``).
|
|
122
|
+
_CANDIDATE_SKELETON = """\
|
|
123
|
+
```json
|
|
124
|
+
[
|
|
125
|
+
{{
|
|
126
|
+
"hypothesis": "<string>",
|
|
127
|
+
"mechanism_type": "<string>",
|
|
128
|
+
"horizon_rationale": "<string>",
|
|
129
|
+
"description": "<string>",
|
|
130
|
+
"code": "<string>"
|
|
131
|
+
}}
|
|
132
|
+
]
|
|
133
|
+
```"""
|
|
134
|
+
|
|
135
|
+
PLAN = """\
|
|
136
|
+
You are the research planner of a search for cross-sectional factors on {market}, each built from a
|
|
137
|
+
stated economic mechanism. You plan this round's research directions; the proposer then writes one
|
|
138
|
+
candidate factor for each research direction you plan and decides how to compute it. So state each
|
|
139
|
+
research direction as the mechanism and the data it reads, and leave the formula, its windows and its
|
|
140
|
+
ratios to the proposer: a direction written as a known formula tends to come back as that textbook
|
|
141
|
+
factor, while a mechanism leaves room for a new way to measure it.
|
|
142
|
+
|
|
143
|
+
Round: {round}
|
|
144
|
+
|
|
145
|
+
<data>
|
|
146
|
+
{data}
|
|
147
|
+
</data>
|
|
148
|
+
|
|
149
|
+
{context}
|
|
150
|
+
|
|
151
|
+
The mechanism types:
|
|
152
|
+
<mechanism_types>
|
|
153
|
+
{mechanisms}
|
|
154
|
+
</mechanism_types>
|
|
155
|
+
|
|
156
|
+
{actions}
|
|
157
|
+
|
|
158
|
+
<quota>
|
|
159
|
+
{quota}
|
|
160
|
+
</quota>
|
|
161
|
+
|
|
162
|
+
Now plan this round's research directions: for each mechanism type, as many as <quota> gives it.
|
|
163
|
+
Each research direction is an object with these fields:
|
|
164
|
+
- "mechanism_type": one of the mechanism types in <quota>
|
|
165
|
+
- "action": "new", "mutation" or "refinement"
|
|
166
|
+
- "parent": the id of the trial a mutation or a refinement builds on, "" for a new research direction
|
|
167
|
+
- "direction": the mechanism to pursue and the data it reads, in one or two sentences
|
|
168
|
+
|
|
169
|
+
Answer with a JSON array of research directions, one element per research direction, each with these
|
|
170
|
+
fields and no others:
|
|
171
|
+
```json
|
|
172
|
+
[
|
|
173
|
+
{{
|
|
174
|
+
"mechanism_type": "<string>",
|
|
175
|
+
"action": "<new, mutation or refinement>",
|
|
176
|
+
"parent": "<string>",
|
|
177
|
+
"direction": "<string>"
|
|
178
|
+
}}
|
|
179
|
+
]
|
|
180
|
+
```
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
#: The planner's actions (``plan``'s ``actions``) when it reads the earlier trials.
|
|
184
|
+
ACTION_RULES = """\
|
|
185
|
+
Each research direction takes one of these actions:
|
|
186
|
+
- "new": a mechanism not tried before; its "parent" is "".
|
|
187
|
+
- "refinement": the mechanism of an earlier evaluated trial, accepted or refused, measured another
|
|
188
|
+
way: to remove what refused it (a residual t below the threshold, a correlation with an existing
|
|
189
|
+
factor, deciles that do not rise in the factor's direction) or to strengthen an accepted factor.
|
|
190
|
+
Its "parent" is that trial's id. Change how the mechanism is measured, because a version that
|
|
191
|
+
changes only the windows or the parameters is refused.
|
|
192
|
+
- "mutation": another economic channel an earlier evaluated trial's result points to, such as the
|
|
193
|
+
mechanism that would explain a factor direction it found the other way round, or a channel
|
|
194
|
+
orthogonal to an accepted factor; its "parent" is that trial's id.
|
|
195
|
+
A trial's id is the T<round>-<slot> that <recent_history>, <last_round> and <best> name it by, such as
|
|
196
|
+
T3-07. The proposer is given the parent trial's code, its statistics and why it was refused. A research
|
|
197
|
+
direction whose parent is not the id of an evaluated trial is planned as a new one."""
|
|
198
|
+
|
|
199
|
+
#: The planner's actions when it reads no earlier trial (``context.feedback`` off).
|
|
200
|
+
ONLY_NEW = """\
|
|
201
|
+
Every research direction is new: its "action" is "new" and its "parent" is ""."""
|
|
202
|
+
|
|
203
|
+
PROPOSE = """\
|
|
204
|
+
You are a quantitative researcher. You write one candidate factor for each research direction in
|
|
205
|
+
<directions>: a cross-sectional factor on {market}, built from a stated economic mechanism, that
|
|
206
|
+
predicts the return over the next {horizon} bars. Each candidate is checked by a static gate, judged
|
|
207
|
+
by a reviewer on whether its hypothesis, description and code agree, and then evaluated on a training
|
|
208
|
+
period.
|
|
209
|
+
|
|
210
|
+
Round: {round}
|
|
211
|
+
|
|
212
|
+
<data>
|
|
213
|
+
{data}
|
|
214
|
+
</data>
|
|
215
|
+
|
|
216
|
+
{context}
|
|
217
|
+
|
|
218
|
+
How to write the code:
|
|
219
|
+
{code_rules}
|
|
220
|
+
|
|
221
|
+
<vocabulary>
|
|
222
|
+
{vocabulary}
|
|
223
|
+
</vocabulary>
|
|
224
|
+
|
|
225
|
+
<directions>
|
|
226
|
+
{directions}
|
|
227
|
+
</directions>
|
|
228
|
+
|
|
229
|
+
Now write one candidate for each research direction in <directions> ({n} in all), in the same order.
|
|
230
|
+
Each candidate is an object with these fields:
|
|
231
|
+
{fields}
|
|
232
|
+
|
|
233
|
+
Answer with a JSON array of candidates, one per research direction ({n} in all). Element i is the
|
|
234
|
+
candidate for research direction i, because the candidates are matched to the research directions by
|
|
235
|
+
position; give each one these fields and no others:
|
|
236
|
+
""" + _CANDIDATE_SKELETON + "\n"
|
|
237
|
+
|
|
238
|
+
RETRY = """\
|
|
239
|
+
You are a quantitative researcher correcting candidate factors that were refused before evaluation.
|
|
240
|
+
Each entry in <refused_candidates> holds the research direction a candidate was written for
|
|
241
|
+
("direction"), the refused version ("refused_version"), why it was refused ("refused_because") and,
|
|
242
|
+
for some, how to write it instead ("how_to_write_it"). An entry the reviewer sent back also holds the
|
|
243
|
+
unit of the refused version's value ("output_unit"), such as "pure number" or "price"; a unit other
|
|
244
|
+
than a pure number is normalised, for example by dividing by a price, unless the mechanism is about
|
|
245
|
+
that level. Your corrected candidates
|
|
246
|
+
go through the same checks again, then to the reviewer and the evaluation on a training period.
|
|
247
|
+
The factors are on {market}; each predicts the return over the next {horizon} bars.
|
|
248
|
+
|
|
249
|
+
Round: {round}
|
|
250
|
+
|
|
251
|
+
How to write the code:
|
|
252
|
+
{code_rules}
|
|
253
|
+
|
|
254
|
+
<vocabulary>
|
|
255
|
+
{vocabulary}
|
|
256
|
+
</vocabulary>
|
|
257
|
+
|
|
258
|
+
<refused_candidates>
|
|
259
|
+
{failures}
|
|
260
|
+
</refused_candidates>
|
|
261
|
+
|
|
262
|
+
Now correct every entry in <refused_candidates> ({n} in all), in the same order. Change the code or the
|
|
263
|
+
fields so that the candidate passes and still expresses its mechanism and the idea of its research
|
|
264
|
+
direction. When the reason is that it duplicates an earlier candidate, write a different mechanism of
|
|
265
|
+
the same mechanism type instead.
|
|
266
|
+
|
|
267
|
+
Return each corrected candidate object itself, an object with these fields:
|
|
268
|
+
{fields}
|
|
269
|
+
Leave out "number", "direction", "output_unit", "refused_because" and "how_to_write_it", and write
|
|
270
|
+
the fields at the top level of the object rather than nested inside another object, because element i
|
|
271
|
+
of your array is read directly as the corrected candidate for entry i.
|
|
272
|
+
|
|
273
|
+
Answer with a JSON array of corrected candidates, one per entry ({n} in all). Element i answers entry
|
|
274
|
+
i of <refused_candidates>; give each one these fields and no others:
|
|
275
|
+
""" + _CANDIDATE_SKELETON + "\n"
|
|
276
|
+
|
|
277
|
+
REVIEW = """\
|
|
278
|
+
You are the reviewer of candidate factors on {market}, each meant to predict the return over the
|
|
279
|
+
next {horizon} bars. You judge, for each candidate in <candidates>, whether its hypothesis, its
|
|
280
|
+
description and its code agree; a candidate you find inconsistent is sent back once to be rewritten
|
|
281
|
+
with your reason and is dropped if it is still inconsistent, and one you find consistent is evaluated
|
|
282
|
+
on a training period.
|
|
283
|
+
|
|
284
|
+
Round: {round}
|
|
285
|
+
|
|
286
|
+
The data, with every field a candidate's code may read and what it holds:
|
|
287
|
+
<data>
|
|
288
|
+
{data}
|
|
289
|
+
</data>
|
|
290
|
+
|
|
291
|
+
Each candidate is listed with its "output_unit": the unit of its value, as the static gate infers it
|
|
292
|
+
from the units of the fields, such as "pure number" (a ratio, a return, a rank or a z-score), "price",
|
|
293
|
+
"money", "volume" or "1/dollars". It is "unknown" when the gate cannot tell; then judge the unit from
|
|
294
|
+
the code.
|
|
295
|
+
|
|
296
|
+
<candidates>
|
|
297
|
+
{candidates}
|
|
298
|
+
</candidates>
|
|
299
|
+
|
|
300
|
+
A candidate is inconsistent when any of these holds:
|
|
301
|
+
1. The code computes something the description does not say.
|
|
302
|
+
2. The description says something the code does not compute.
|
|
303
|
+
3. The code leaves out an input or a step the hypothesis needs.
|
|
304
|
+
4. What the code computes is not the quantity the hypothesis says drives the return.
|
|
305
|
+
5. The code does not use an input, a transformation or a quantity the hypothesis says the factor is
|
|
306
|
+
built from. What the hypothesis mentions only as background, as a consequence or as a contrast
|
|
307
|
+
does not count, because the factor need not be built from it. For example, a hypothesis that
|
|
308
|
+
prices under-react to earnings surprises, with code that reads only price and volume, is
|
|
309
|
+
inconsistent; a hypothesis that investors watch net income and overlook gross profitability,
|
|
310
|
+
with code built from gross profit, is consistent, because net income is only the contrast.
|
|
311
|
+
6. The output has a unit other than a pure number ("output_unit"), and the hypothesis does not
|
|
312
|
+
explain why the cross-section is ranked by that level, because such a value ranks it partly by
|
|
313
|
+
price level or size. For example, the Amihud illiquidity, the absolute return per dollar traded
|
|
314
|
+
(1/dollars), is consistent when its hypothesis is about the price impact per dollar traded; a
|
|
315
|
+
slope of vwap described as a trend, without dividing by a price (price), is inconsistent.
|
|
316
|
+
7. The code fills missing values (SetInfOrNanToValue) and the description does not state the reason,
|
|
317
|
+
what a missing value means in the mechanism, because a filled value is ranked as real data. For
|
|
318
|
+
example, R&D expense filled with 0 is consistent when the description says an unreported R&D
|
|
319
|
+
expense is zero; a ratio filled with 0 for no stated reason is inconsistent.
|
|
320
|
+
Otherwise it is consistent. Which way a factor predicts (its factor direction) is read from the data
|
|
321
|
+
afterwards, so a factor direction the text states, leaves out or gets the other way round is no reason
|
|
322
|
+
to find a candidate inconsistent. The length of the code is no reason to find a candidate consistent
|
|
323
|
+
or inconsistent: judge what the code computes. Your verdict is the whole answer, so judge each
|
|
324
|
+
candidate as it is written.
|
|
325
|
+
|
|
326
|
+
Now judge every candidate in <candidates>. Answer with a JSON array of verdicts, one per candidate:
|
|
327
|
+
element i is the verdict on candidate i, with that candidate's "number", a one-sentence "reason", and
|
|
328
|
+
then "consistent" (true or false). Give each verdict these fields and no others:
|
|
329
|
+
```json
|
|
330
|
+
[
|
|
331
|
+
{{
|
|
332
|
+
"number": <integer>,
|
|
333
|
+
"reason": "<one sentence>",
|
|
334
|
+
"consistent": <true or false>
|
|
335
|
+
}}
|
|
336
|
+
]
|
|
337
|
+
```
|
|
338
|
+
"""
|
|
339
|
+
|
|
340
|
+
#: The parts of the memory, by heading, in order: what the distiller is asked for (``DISTILL``) and
|
|
341
|
+
#: what its reply is parsed by (``factor_agent.researcher``). A replaced ``distill`` template must name each.
|
|
342
|
+
MEMORY_PARTS = ("Working mechanism patterns", "Failure constraints", "Directional findings")
|
|
343
|
+
#: Items a part of the memory keeps at most.
|
|
344
|
+
MEMORY_ITEMS = 6
|
|
345
|
+
|
|
346
|
+
DISTILL = """\
|
|
347
|
+
You are the keeper of the memory of a search for cross-sectional factors on {market}; the factors
|
|
348
|
+
predict the return over the next {horizon} bars. The planner and the proposer of the next rounds read
|
|
349
|
+
your memory in their context, next to the latest feedback, to choose which mechanisms to pursue and
|
|
350
|
+
how to write them, so it carries the lessons that the list of trials is too long to show. You receive
|
|
351
|
+
the previous memory, the trials since it was written and every factor accepted so far, and write the
|
|
352
|
+
next memory from them.
|
|
353
|
+
|
|
354
|
+
Round: {round}
|
|
355
|
+
|
|
356
|
+
<previous_memory>
|
|
357
|
+
{memory}
|
|
358
|
+
</previous_memory>
|
|
359
|
+
|
|
360
|
+
<accepted>
|
|
361
|
+
{accepted}
|
|
362
|
+
</accepted>
|
|
363
|
+
|
|
364
|
+
<history>
|
|
365
|
+
{history}
|
|
366
|
+
</history>
|
|
367
|
+
|
|
368
|
+
""" + f"""Now fold the new trials in <history> into the previous memory, in three parts:
|
|
369
|
+
- {MEMORY_PARTS[0]}: the kinds of mechanism and construction that were accepted, each with
|
|
370
|
+
why it worked; only the factors in <accepted> were, so this part holds those and nothing else,
|
|
371
|
+
and while <accepted> holds none it is the one item "- none accepted yet: <what the trials
|
|
372
|
+
suggest to try next>". Drop an item of the previous memory that is about no factor in <accepted>.
|
|
373
|
+
A factor a refusal names, a correlation with it being too high, is a factor that existed before
|
|
374
|
+
the search (of the reference set, or known) or one in <accepted>: it shows what a candidate
|
|
375
|
+
repeated, not one this search accepted.
|
|
376
|
+
- {MEMORY_PARTS[1]}: what keeps being refused, each as a rule to follow with its reason; a rule
|
|
377
|
+
needs refusals of several trials for the same reason, and a mechanism refused once or twice is
|
|
378
|
+
not yet one to avoid.
|
|
379
|
+
- {MEMORY_PARTS[2]}: which way the evaluated mechanisms worked, as the factor directions in
|
|
380
|
+
<history> show them, such as "volume surges were followed by outperformance".
|
|
381
|
+
Keep what the new trials do not contradict, merge items that say the same thing, and drop a finding
|
|
382
|
+
the new trials overturned. Write at most {MEMORY_ITEMS} items per part, the most useful first; later items are cut. Describe every pattern in words, because the next rounds
|
|
383
|
+
should reason about mechanisms rather than tune to numbers; so the memory holds no numbers,
|
|
384
|
+
statistics or factor code.
|
|
385
|
+
|
|
386
|
+
Answer with the memory as plain text, without a code fence, in this shape:
|
|
387
|
+
```text
|
|
388
|
+
{MEMORY_PARTS[0]}
|
|
389
|
+
- <pattern>: <why it worked>
|
|
390
|
+
{MEMORY_PARTS[1]}
|
|
391
|
+
- <rule>: <why>
|
|
392
|
+
{MEMORY_PARTS[2]}
|
|
393
|
+
- <finding>
|
|
394
|
+
```
|
|
395
|
+
"""
|
|
396
|
+
|
|
397
|
+
#: The context sections in prompt order, by key and heading; an empty section is left out. Each is
|
|
398
|
+
#: shown under its heading inside the XML tag named by its key.
|
|
399
|
+
SECTIONS = (
|
|
400
|
+
("seed", "Seed research directions"),
|
|
401
|
+
("families", "Existing factor families (candidates must add to them, not rewrite them)"),
|
|
402
|
+
("memory", "Memory distilled from earlier rounds"),
|
|
403
|
+
("earlier_summary", "Earlier summary (the older trials, counted by mechanism type and outcome)"),
|
|
404
|
+
("themes", "What the evaluated trials measure (the trials grouped by the correlation of their factors)"),
|
|
405
|
+
("recent_history", "Recent history (the trials of the last rounds, one line each)"),
|
|
406
|
+
("last_round", "Last round's feedback (training period only)"),
|
|
407
|
+
("best", "Current best mechanisms (the accepted factors)"),
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
#: The built-in templates, by the name a config's ``prompts`` replaces them under.
|
|
411
|
+
TEMPLATES = {
|
|
412
|
+
"data": DATA, "plan": PLAN, "propose": PROPOSE, "retry": RETRY, "review": REVIEW, "distill": DISTILL,
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
#: What every template may use.
|
|
416
|
+
_COMMON = frozenset({"market", "horizon"})
|
|
417
|
+
|
|
418
|
+
#: Per template, the placeholders it must use and the ones it may use besides ``_COMMON``.
|
|
419
|
+
PLACEHOLDERS = {
|
|
420
|
+
"data": (frozenset({"market", "inputs", "scoring", "horizon"}), frozenset()),
|
|
421
|
+
"plan": (frozenset({"data", "context", "quota"}), frozenset({"round", "mechanisms", "actions"})),
|
|
422
|
+
"propose": (
|
|
423
|
+
frozenset({"data", "context", "code_rules", "vocabulary", "fields", "n", "directions"}),
|
|
424
|
+
frozenset({"round", "mechanisms"}),
|
|
425
|
+
),
|
|
426
|
+
"retry": (
|
|
427
|
+
frozenset({"code_rules", "vocabulary", "fields", "n", "failures"}), frozenset({"round", "data", "mechanisms"}),
|
|
428
|
+
),
|
|
429
|
+
"review": (frozenset({"candidates", "data"}), frozenset({"round", "mechanisms"})),
|
|
430
|
+
"distill": (frozenset({"memory", "history"}), frozenset({"round", "data", "mechanisms", "accepted"})),
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
class PromptError(ValueError):
|
|
435
|
+
"""Replaced templates are wrong; the message lists every problem."""
|
|
436
|
+
|
|
437
|
+
def __init__(self, problems: list[str]):
|
|
438
|
+
self.problems = tuple(problems)
|
|
439
|
+
super().__init__("the prompt templates have problems:\n" + "\n".join(f"- {p}" for p in problems))
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
class Templates:
|
|
443
|
+
"""The templates of a campaign: the built-in ones, any of them replaced from a file."""
|
|
444
|
+
|
|
445
|
+
def __init__(self, texts: Mapping[str, str]):
|
|
446
|
+
self._texts = {**TEMPLATES, **texts}
|
|
447
|
+
|
|
448
|
+
@classmethod
|
|
449
|
+
def load(cls, replaced: Mapping[str, str | Path]) -> "Templates":
|
|
450
|
+
"""The built-in templates with those of ``replaced`` (template name to file) read from their files.
|
|
451
|
+
|
|
452
|
+
Raises
|
|
453
|
+
------
|
|
454
|
+
PromptError
|
|
455
|
+
Naming every unknown template, unreadable file, malformed template, and
|
|
456
|
+
placeholder a template lacks or may not use.
|
|
457
|
+
"""
|
|
458
|
+
problems, texts = [], {}
|
|
459
|
+
for name, path in replaced.items():
|
|
460
|
+
if name not in TEMPLATES:
|
|
461
|
+
problems.append(f"{name}: not a template; the templates are {list(TEMPLATES)}")
|
|
462
|
+
continue
|
|
463
|
+
try:
|
|
464
|
+
texts[name] = Path(path).read_text()
|
|
465
|
+
except OSError as exc:
|
|
466
|
+
problems.append(f"{name} ({path}): cannot be read ({exc})")
|
|
467
|
+
continue
|
|
468
|
+
problems += [f"{name} ({path}): {problem}" for problem in _placeholder_problems(name, texts[name])]
|
|
469
|
+
if problems:
|
|
470
|
+
raise PromptError(problems)
|
|
471
|
+
return cls(texts)
|
|
472
|
+
|
|
473
|
+
def format(self, name: str, **values) -> str:
|
|
474
|
+
"""Template ``name`` filled with ``values``; values it does not name are ignored."""
|
|
475
|
+
return self._texts[name].format(**values)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
@dataclass(frozen=True)
|
|
479
|
+
class Prompts:
|
|
480
|
+
"""What the Researcher's prompts say: the templates and what fills every one of them.
|
|
481
|
+
|
|
482
|
+
``market`` is the config's description of the data, ``horizon`` the
|
|
483
|
+
bars the label spans, ``scoring`` the scoring's sentence
|
|
484
|
+
(``Scoring.describe``), ``vocabulary`` what candidates write with and
|
|
485
|
+
``mechanisms`` the mechanism types with their one-line descriptions, in order.
|
|
486
|
+
"""
|
|
487
|
+
|
|
488
|
+
templates: Templates
|
|
489
|
+
market: str
|
|
490
|
+
horizon: int
|
|
491
|
+
scoring: str
|
|
492
|
+
vocabulary: Vocabulary
|
|
493
|
+
mechanisms: Mapping[str, str]
|
|
494
|
+
|
|
495
|
+
@property
|
|
496
|
+
def mechanism_types(self) -> tuple[str, ...]:
|
|
497
|
+
"""The names of the mechanism types, in order: what a round is planned over and a candidate belongs to."""
|
|
498
|
+
return tuple(self.mechanisms)
|
|
499
|
+
|
|
500
|
+
def render(self, name: str, **values) -> str:
|
|
501
|
+
"""Template ``name`` filled with ``values`` and every value common to the templates."""
|
|
502
|
+
common = {"market": self.market, "horizon": self.horizon}
|
|
503
|
+
common["data"] = self.templates.format(
|
|
504
|
+
"data", **common, scoring=self.scoring, inputs=inputs(self.vocabulary),
|
|
505
|
+
)
|
|
506
|
+
common.update(
|
|
507
|
+
mechanisms="\n".join(f"- {kind}: {text}" for kind, text in self.mechanisms.items()),
|
|
508
|
+
code_rules=code_rules(self.vocabulary), vocabulary=VOCABULARY.format(**self.vocabulary.prompt()),
|
|
509
|
+
fields=FIELDS.format(horizon=self.horizon),
|
|
510
|
+
)
|
|
511
|
+
return self.templates.format(name, **common, **values)
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def code_rules(vocabulary: Vocabulary) -> str:
|
|
515
|
+
"""How to write the code: ``CODE_RULES``, and ``INTRADAY`` with its examples when an intraday family is open."""
|
|
516
|
+
if vocabulary.intraday_family is None:
|
|
517
|
+
return CODE_RULES
|
|
518
|
+
examples = "\n".join(
|
|
519
|
+
f"Example {number}: {description}:\n" + "\n".join(f" {line}" for line in code.splitlines())
|
|
520
|
+
for number, (description, code) in enumerate(INTRADAY_EXAMPLES, 1)
|
|
521
|
+
)
|
|
522
|
+
return CODE_RULES + INTRADAY.format(
|
|
523
|
+
family=vocabulary.intraday_family, windows=", ".join(map(str, vocabulary.intraday_windows)),
|
|
524
|
+
lags=", ".join(map(str, vocabulary.intraday_lags)), examples=examples,
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def inputs(vocabulary: Vocabulary) -> str:
|
|
529
|
+
"""The fields of ``vocabulary`` by family, each with its unit (a derived field's follows from its formula)."""
|
|
530
|
+
lines = []
|
|
531
|
+
for family, description in vocabulary.families.items():
|
|
532
|
+
marked = f"{family}, the intraday family" if family == vocabulary.intraday_family else family
|
|
533
|
+
lines.append(f"- {marked}" + (f" ({description}):" if description else ":"))
|
|
534
|
+
for field in vocabulary.fields.values():
|
|
535
|
+
if field.family == family:
|
|
536
|
+
unit = f" ({_dimension.name(field.dimension)})" if field.dimension is not None else ""
|
|
537
|
+
lines.append(f" - {field.name}{unit}: {field.description}")
|
|
538
|
+
return "\n".join(lines)
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def _placeholder_problems(name: str, text: str) -> list[str]:
|
|
542
|
+
"""What is wrong with the placeholders of template ``name``'s ``text``."""
|
|
543
|
+
try:
|
|
544
|
+
used = {field for _, field, _, _ in string.Formatter().parse(text) if field is not None}
|
|
545
|
+
except ValueError as exc:
|
|
546
|
+
return [f"is not a valid template ({exc}); a literal brace is written twice"]
|
|
547
|
+
required, optional = PLACEHOLDERS[name]
|
|
548
|
+
problems = [f"lacks the placeholder {{{p}}}" for p in sorted(required - used)]
|
|
549
|
+
if name == "distill": # the distiller's reply is parsed by these headings, so it must be asked for them
|
|
550
|
+
problems += [f"lacks the memory part heading {part!r}" for part in MEMORY_PARTS if part not in text]
|
|
551
|
+
unknown = sorted(used - required - optional - _COMMON)
|
|
552
|
+
if unknown:
|
|
553
|
+
problems.append(
|
|
554
|
+
f"uses the unknown placeholder(s) {['{' + p + '}' for p in unknown]}; it may use "
|
|
555
|
+
f"{sorted(required | optional | _COMMON)}"
|
|
556
|
+
)
|
|
557
|
+
return problems
|