PineForge HPO 1.11.0
Native hyperparameter optimization for PineForge strategies
Loading...
Searching...
No Matches
symbol_bindings.py
Go to the documentation of this file.
1"""Internal, pure validation of declared symbol domains and data identities."""
2
3from __future__ import annotations
4
5from dataclasses import dataclass, replace
6import math
7import re
8from typing import AbstractSet, Mapping, NoReturn, Sequence
9
10from .cli import _cli_error
11from .study_spec import ParameterSpec
12
13
14@dataclass(frozen=True)
16 """Declared instrument facts; these do not attest to an engine's report unit."""
17
18 tickerid: str
19 type: str
20 currency: str
21 mintick: float
22 pointvalue: float
23 mincontract: float
24 timezone: str
25 session: str
26
27
28@dataclass(frozen=True)
30 """Identity and interpretation of a tape, independent of its filesystem path."""
31
32 id: str
33 symbol: str
34 input_tf: str
35 script_tf: str
36 chart_timezone: str
37 syminfo: SymbolFacts
38
39
40@dataclass(frozen=True)
42 """A role and an explicit choice-to-dataset map.
43
44 Pairs retain duplicate keys until validation; converting an unvalidated JSON object
45 straight to a dict would lose that evidence.
46 """
47
48 input_title: str
49 role: str
50 datasets: tuple[tuple[object, str], ...]
51
52
53@dataclass(frozen=True)
55 """Validated datasets in effective categorical choice order."""
56
57 input_title: str
58 role: str
59 choices: tuple[str, ...]
60 datasets: tuple[DatasetDeclaration, ...]
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
124 bindings: Sequence[SymbolBinding],
125 *,
126 search_space: Mapping[str, ParameterSpec],
127 referenced_dataset_ids: Sequence[str],
128 datasets: Sequence[DatasetDeclaration],
129) -> tuple[ResolvedSymbolBinding, ...]:
130 """Check a complete effective domain without loading data or sampling candidates.
131
132 The caller supplies the effective search space after its ordinary input validation.
133 This component neither admits new StudySpec fields nor bypasses manifest validation.
134 Additional dataset declarations are checked but need not be binding targets.
135 """
136
137 declarations: dict[str, DatasetDeclaration] = {}
138 for declaration in datasets:
139 normalized = _dataset(declaration)
140 if normalized.id in declarations:
141 _invalid(
142 "input_symbol", f"datasets: duplicate dataset id {normalized.id!r}"
143 )
144 declarations[normalized.id] = normalized
145
146 references: set[str] = set()
147 for dataset_id in referenced_dataset_ids:
148 _text(dataset_id, "strategy.datasets", "input_symbol")
149 if dataset_id in references:
150 _invalid(
151 "input_symbol", f"strategy.datasets: duplicates include {dataset_id!r}"
152 )
153 if dataset_id not in declarations:
154 _invalid(
155 "input_symbol", f"strategy.datasets: unknown dataset {dataset_id!r}"
156 )
157 references.add(dataset_id)
158
159 titles: set[str] = set()
160 resolved: list[ResolvedSymbolBinding] = []
161 for binding in bindings:
162 where = f"symbol_bindings.{binding.input_title}"
163 _text(binding.input_title, where, "input_symbol")
164 if binding.input_title in titles:
165 _invalid("input_symbol", f"{where}: duplicate binding input")
166 titles.add(binding.input_title)
167 if binding.role not in ("chart", "request"):
168 _invalid("input_symbol", f"{where}.role: must be chart or request")
169 parameter = search_space.get(binding.input_title)
170 if parameter is None or parameter.kind != "categorical":
171 _invalid(
172 "input_symbol",
173 f"{where}: requires an effective categorical search input",
174 )
175 if not parameter.choices:
176 _invalid("input_symbol", f"{where}.choices: domain must not be empty")
177 choices: list[str] = []
178 seen: set[str] = set()
179 for choice in parameter.choices:
180 if not isinstance(choice, str):
181 _invalid(
182 "input_symbol",
183 f"{where}.choices: symbol choice {choice!r} is not a string",
184 )
185 _text(choice, f"{where}.choices", "input_symbol")
186 if choice in seen:
187 _invalid(
188 "input_symbol", f"{where}.choices: duplicate choice {choice!r}"
189 )
190 seen.add(choice)
191 choices.append(choice)
192
193 mapping: dict[str, str] = {}
194 targets: set[str] = set()
195 for choice, dataset_id in binding.datasets:
196 _text(choice, f"{where}.datasets choice", "input_symbol")
197 _text(dataset_id, f"{where}.datasets[{choice!r}]", "input_symbol")
198 if choice in mapping:
199 _invalid("input_symbol", f"{where}.datasets: duplicate key {choice!r}")
200 mapping[choice] = dataset_id
201 if set(mapping) != seen:
202 _invalid(
203 "input_symbol",
204 f"{where}.datasets: keys must equal choices exactly; "
205 f"missing={sorted(seen - set(mapping))!r}, extra={sorted(set(mapping) - seen)!r}",
206 )
207 selected: list[DatasetDeclaration] = []
208 for choice in choices:
209 dataset_id = mapping[choice]
210 if dataset_id in targets:
211 _invalid(
212 "input_symbol", f"{where}.datasets: duplicate target {dataset_id!r}"
213 )
214 targets.add(dataset_id)
215 if dataset_id not in declarations:
216 _invalid(
217 "input_symbol",
218 f"{where}.datasets[{choice!r}]: unknown dataset {dataset_id!r}",
219 )
220 if dataset_id not in references:
221 _invalid(
222 "input_symbol",
223 f"{where}.datasets[{choice!r}]: unreferenced dataset {dataset_id!r}",
224 )
225 declaration = declarations[dataset_id]
226 if declaration.symbol != choice:
227 _invalid(
228 "input_symbol",
229 f"{where}.datasets[{choice!r}]: dataset {dataset_id!r} declares "
230 f"symbol {declaration.symbol!r}",
231 )
232 selected.append(declaration)
233 resolved.append(
235 binding.input_title, binding.role, tuple(choices), tuple(selected)
236 )
237 )
238 if bindings and sum(binding.role == "chart" for binding in bindings) != 1:
239 _invalid("input_symbol", "symbol_bindings: exactly one chart role is required")
240 return tuple(resolved)
241
242
244 bindings: Sequence[ResolvedSymbolBinding],
245 *,
246 pruner: str,
247 alignment_mode: str,
248 approved_alignment_modes: AbstractSet[str],
249) -> None:
250 """Refuse undecided policy combinations; callers must supply all approved modes."""
251
252 if not bindings:
253 return
254 if pruner != "none":
255 _invalid(
256 "pruner", f"execution.pruner {pruner!r}: undecided policy for bound data"
257 )
258 currencies: dict[str, str] = {}
259 for binding in bindings:
260 for declaration in binding.datasets:
261 currencies[declaration.id] = declaration.syminfo.currency
262 if len(set(currencies.values())) > 1:
263 _invalid(
264 "objective",
265 f"objective: undecided quote currency ranking policy for datasets {currencies!r}",
266 )
267 if alignment_mode not in approved_alignment_modes:
268 _invalid(
269 "backtest",
270 f"data_comparison.alignment {alignment_mode!r}: missing approval in approved modes",
271 )
tuple[ResolvedSymbolBinding,...] validate_symbol_bindings(Sequence[SymbolBinding] bindings, *Mapping[str, ParameterSpec] search_space, Sequence[str] referenced_dataset_ids, Sequence[DatasetDeclaration] datasets)
None validate_bound_policy(Sequence[ResolvedSymbolBinding] bindings, *str pruner, str alignment_mode, AbstractSet[str] approved_alignment_modes)