opencode-pyneruntime 6.6.4__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.
Files changed (261) hide show
  1. opencode_pyneruntime-6.6.4.dist-info/METADATA +281 -0
  2. opencode_pyneruntime-6.6.4.dist-info/RECORD +261 -0
  3. opencode_pyneruntime-6.6.4.dist-info/WHEEL +5 -0
  4. opencode_pyneruntime-6.6.4.dist-info/entry_points.txt +6 -0
  5. opencode_pyneruntime-6.6.4.dist-info/licenses/LICENSE +201 -0
  6. opencode_pyneruntime-6.6.4.dist-info/licenses/NOTICE +21 -0
  7. opencode_pyneruntime-6.6.4.dist-info/top_level.txt +1 -0
  8. pynecore/__init__.py +6 -0
  9. pynecore/cli/__init__.py +2 -0
  10. pynecore/cli/app.py +238 -0
  11. pynecore/cli/commands/__init__.py +343 -0
  12. pynecore/cli/commands/benchmark.py +186 -0
  13. pynecore/cli/commands/compile.py +198 -0
  14. pynecore/cli/commands/data.py +857 -0
  15. pynecore/cli/commands/debug.py +63 -0
  16. pynecore/cli/commands/optimize.py +956 -0
  17. pynecore/cli/commands/plugin.py +242 -0
  18. pynecore/cli/commands/run.py +2006 -0
  19. pynecore/cli/pluggable.py +132 -0
  20. pynecore/cli/utils/__init__.py +0 -0
  21. pynecore/cli/utils/api_error_handler.py +168 -0
  22. pynecore/cli/utils/broker_picker.py +330 -0
  23. pynecore/cli/utils/error_hook.py +28 -0
  24. pynecore/cli/utils/keyreader.py +178 -0
  25. pynecore/cli/utils/provider_picker.py +19 -0
  26. pynecore/cli/utils/symbol_browser.py +1149 -0
  27. pynecore/core/__init__.py +0 -0
  28. pynecore/core/aggregator.py +257 -0
  29. pynecore/core/bar_magnifier.py +168 -0
  30. pynecore/core/broker/__init__.py +64 -0
  31. pynecore/core/broker/defaults.py +113 -0
  32. pynecore/core/broker/disappearance.py +927 -0
  33. pynecore/core/broker/emulator.py +345 -0
  34. pynecore/core/broker/exceptions.py +346 -0
  35. pynecore/core/broker/idempotency.py +401 -0
  36. pynecore/core/broker/intent_builder.py +334 -0
  37. pynecore/core/broker/journal.py +1785 -0
  38. pynecore/core/broker/models.py +1600 -0
  39. pynecore/core/broker/native_failsafe_manager.py +1436 -0
  40. pynecore/core/broker/one_way_emulator.py +1128 -0
  41. pynecore/core/broker/position.py +787 -0
  42. pynecore/core/broker/run_identity.py +126 -0
  43. pynecore/core/broker/software_entry_stop_engine.py +351 -0
  44. pynecore/core/broker/software_partial_bracket_engine.py +1379 -0
  45. pynecore/core/broker/spot_inventory.py +1327 -0
  46. pynecore/core/broker/storage.py +2655 -0
  47. pynecore/core/broker/store_helpers.py +2161 -0
  48. pynecore/core/broker/sync_engine.py +16070 -0
  49. pynecore/core/broker/validation.py +382 -0
  50. pynecore/core/class_property.py +7 -0
  51. pynecore/core/config.py +392 -0
  52. pynecore/core/csv_file.py +547 -0
  53. pynecore/core/currency.py +262 -0
  54. pynecore/core/data_converter.py +1002 -0
  55. pynecore/core/datetime.py +296 -0
  56. pynecore/core/download_info.py +71 -0
  57. pynecore/core/download_runner.py +274 -0
  58. pynecore/core/htf_aggregator.py +181 -0
  59. pynecore/core/import_hook.py +358 -0
  60. pynecore/core/instance_state.py +494 -0
  61. pynecore/core/live_ltf_collector.py +442 -0
  62. pynecore/core/live_ltf_window.py +189 -0
  63. pynecore/core/live_runner.py +1347 -0
  64. pynecore/core/module_property.py +26 -0
  65. pynecore/core/ohlcv_file.py +1888 -0
  66. pynecore/core/overload.py +371 -0
  67. pynecore/core/pine_cast.py +113 -0
  68. pynecore/core/pine_export.py +95 -0
  69. pynecore/core/pine_method.py +244 -0
  70. pynecore/core/pine_range.py +86 -0
  71. pynecore/core/pine_udt.py +69 -0
  72. pynecore/core/plugin/__init__.py +394 -0
  73. pynecore/core/plugin/broker.py +781 -0
  74. pynecore/core/plugin/cli.py +96 -0
  75. pynecore/core/plugin/live_provider.py +208 -0
  76. pynecore/core/plugin/provider.py +331 -0
  77. pynecore/core/provider_string.py +148 -0
  78. pynecore/core/random.py +40 -0
  79. pynecore/core/resampler.py +686 -0
  80. pynecore/core/safe_convert.py +64 -0
  81. pynecore/core/script.py +1011 -0
  82. pynecore/core/script_runner.py +3202 -0
  83. pynecore/core/security.py +1749 -0
  84. pynecore/core/security_process.py +1253 -0
  85. pynecore/core/security_shm.py +456 -0
  86. pynecore/core/series.py +417 -0
  87. pynecore/core/strategy_stats.py +669 -0
  88. pynecore/core/symbol_map.py +134 -0
  89. pynecore/core/syminfo.py +505 -0
  90. pynecore/core/viz.py +591 -0
  91. pynecore/lib/__init__.py +1771 -0
  92. pynecore/lib/_fixnan.py +32 -0
  93. pynecore/lib/_math_stateful.py +202 -0
  94. pynecore/lib/_timeframe_change.py +101 -0
  95. pynecore/lib/adjustment.py +6 -0
  96. pynecore/lib/alert.py +39 -0
  97. pynecore/lib/alert.pyi +14 -0
  98. pynecore/lib/array.py +1051 -0
  99. pynecore/lib/barmerge.py +60 -0
  100. pynecore/lib/barstate.py +30 -0
  101. pynecore/lib/box.py +415 -0
  102. pynecore/lib/chart.py +128 -0
  103. pynecore/lib/color.py +152 -0
  104. pynecore/lib/color.pyi +50 -0
  105. pynecore/lib/currency.py +62 -0
  106. pynecore/lib/dayofweek.py +36 -0
  107. pynecore/lib/dayofweek.pyi +18 -0
  108. pynecore/lib/display.py +8 -0
  109. pynecore/lib/dividends.py +9 -0
  110. pynecore/lib/earnings.py +11 -0
  111. pynecore/lib/extend.py +6 -0
  112. pynecore/lib/font.py +5 -0
  113. pynecore/lib/footprint.py +79 -0
  114. pynecore/lib/format.py +11 -0
  115. pynecore/lib/hline.py +67 -0
  116. pynecore/lib/hline.pyi +24 -0
  117. pynecore/lib/label.py +409 -0
  118. pynecore/lib/line.py +433 -0
  119. pynecore/lib/linefill.py +93 -0
  120. pynecore/lib/location.py +11 -0
  121. pynecore/lib/log.py +362 -0
  122. pynecore/lib/map.py +150 -0
  123. pynecore/lib/math.py +385 -0
  124. pynecore/lib/matrix.py +708 -0
  125. pynecore/lib/order.py +8 -0
  126. pynecore/lib/pivotpointtype.py +8 -0
  127. pynecore/lib/plot.py +95 -0
  128. pynecore/lib/plot.pyi +33 -0
  129. pynecore/lib/polyline.py +91 -0
  130. pynecore/lib/position.py +15 -0
  131. pynecore/lib/request.py +281 -0
  132. pynecore/lib/runtime.py +5 -0
  133. pynecore/lib/scale.py +9 -0
  134. pynecore/lib/session.py +267 -0
  135. pynecore/lib/session.pyi +12 -0
  136. pynecore/lib/shape.py +18 -0
  137. pynecore/lib/size.py +12 -0
  138. pynecore/lib/splits.py +4 -0
  139. pynecore/lib/strategy/__init__.py +4778 -0
  140. pynecore/lib/strategy/closedtrades.py +347 -0
  141. pynecore/lib/strategy/closedtrades.pyi +53 -0
  142. pynecore/lib/strategy/commission.py +9 -0
  143. pynecore/lib/strategy/direction.py +9 -0
  144. pynecore/lib/strategy/oca.py +13 -0
  145. pynecore/lib/strategy/opentrades.py +281 -0
  146. pynecore/lib/strategy/opentrades.pyi +49 -0
  147. pynecore/lib/strategy/risk.py +109 -0
  148. pynecore/lib/string.py +649 -0
  149. pynecore/lib/syminfo.py +84 -0
  150. pynecore/lib/ta.py +2230 -0
  151. pynecore/lib/table.py +290 -0
  152. pynecore/lib/text.py +17 -0
  153. pynecore/lib/ticker.py +207 -0
  154. pynecore/lib/timeframe.py +293 -0
  155. pynecore/lib/volume_row.py +67 -0
  156. pynecore/lib/xloc.py +4 -0
  157. pynecore/lib/yloc.py +5 -0
  158. pynecore/providers/__init__.py +0 -0
  159. pynecore/providers/ccxt.py +664 -0
  160. pynecore/providers/replay.py +187 -0
  161. pynecore/pynesys/__init__.py +0 -0
  162. pynecore/pynesys/api.py +498 -0
  163. pynecore/pynesys/compiler.py +112 -0
  164. pynecore/standalone.py +99 -0
  165. pynecore/testing/__init__.py +1 -0
  166. pynecore/testing/broker_lab/__init__.py +41 -0
  167. pynecore/testing/broker_lab/__main__.py +5 -0
  168. pynecore/testing/broker_lab/cli.py +87 -0
  169. pynecore/testing/broker_lab/generate.py +47 -0
  170. pynecore/testing/broker_lab/model.py +84 -0
  171. pynecore/testing/broker_lab/reference.py +645 -0
  172. pynecore/testing/broker_lab/runner.py +372 -0
  173. pynecore/testing/broker_lab/scheduler.py +50 -0
  174. pynecore/testing/broker_lab/subprocess.py +73 -0
  175. pynecore/transformers/__init__.py +0 -0
  176. pynecore/transformers/builtin_shadow.py +136 -0
  177. pynecore/transformers/closure_arguments_transformer.py +428 -0
  178. pynecore/transformers/display_rewrite.py +140 -0
  179. pynecore/transformers/dynamic_default.py +147 -0
  180. pynecore/transformers/function_isolation.py +757 -0
  181. pynecore/transformers/import_lifter.py +61 -0
  182. pynecore/transformers/import_normalizer.py +328 -0
  183. pynecore/transformers/inline_series_hoist.py +178 -0
  184. pynecore/transformers/input_transformer.py +175 -0
  185. pynecore/transformers/lib_series.py +201 -0
  186. pynecore/transformers/locations.py +70 -0
  187. pynecore/transformers/module_properties.json +3387 -0
  188. pynecore/transformers/module_property.py +221 -0
  189. pynecore/transformers/ne_guard.py +70 -0
  190. pynecore/transformers/persistent.py +320 -0
  191. pynecore/transformers/persistent_series.py +76 -0
  192. pynecore/transformers/safe_convert_transformer.py +97 -0
  193. pynecore/transformers/safe_division_transformer.py +95 -0
  194. pynecore/transformers/script_requirements.py +308 -0
  195. pynecore/transformers/security.py +752 -0
  196. pynecore/transformers/security_instantiation.py +274 -0
  197. pynecore/transformers/series.py +275 -0
  198. pynecore/transformers/slot_layout.py +381 -0
  199. pynecore/transformers/type_checking_stripper.py +25 -0
  200. pynecore/transformers/unused_series_detector.py +267 -0
  201. pynecore/types/__init__.py +21 -0
  202. pynecore/types/alert.py +5 -0
  203. pynecore/types/barmerge.py +5 -0
  204. pynecore/types/base.py +39 -0
  205. pynecore/types/box.py +37 -0
  206. pynecore/types/chart.py +17 -0
  207. pynecore/types/color.py +107 -0
  208. pynecore/types/currency.py +5 -0
  209. pynecore/types/datetime.py +6 -0
  210. pynecore/types/display.py +5 -0
  211. pynecore/types/dividends.py +5 -0
  212. pynecore/types/earnings.py +5 -0
  213. pynecore/types/extend.py +5 -0
  214. pynecore/types/font.py +5 -0
  215. pynecore/types/footprint.py +41 -0
  216. pynecore/types/format.py +5 -0
  217. pynecore/types/hline.py +24 -0
  218. pynecore/types/ib_persistent.py +8 -0
  219. pynecore/types/ib_persistent.pyi +10 -0
  220. pynecore/types/label.py +35 -0
  221. pynecore/types/line.py +32 -0
  222. pynecore/types/linefill.py +13 -0
  223. pynecore/types/location.py +5 -0
  224. pynecore/types/matrix.py +999 -0
  225. pynecore/types/na.py +237 -0
  226. pynecore/types/na.pyi +83 -0
  227. pynecore/types/ohlcv.py +12 -0
  228. pynecore/types/order.py +5 -0
  229. pynecore/types/persistent.py +8 -0
  230. pynecore/types/persistent.pyi +13 -0
  231. pynecore/types/pine_types.py +11 -0
  232. pynecore/types/pine_types.pyi +15 -0
  233. pynecore/types/pivotpointtype.py +5 -0
  234. pynecore/types/plot.py +12 -0
  235. pynecore/types/plot_meta.py +60 -0
  236. pynecore/types/polyline.py +40 -0
  237. pynecore/types/position.py +5 -0
  238. pynecore/types/scale.py +5 -0
  239. pynecore/types/script_type.py +15 -0
  240. pynecore/types/series.py +23 -0
  241. pynecore/types/series.pyi +19 -0
  242. pynecore/types/session.py +35 -0
  243. pynecore/types/shape.py +5 -0
  244. pynecore/types/size.py +5 -0
  245. pynecore/types/source.py +33 -0
  246. pynecore/types/splits.py +5 -0
  247. pynecore/types/strategy.py +45 -0
  248. pynecore/types/table.py +87 -0
  249. pynecore/types/text.py +13 -0
  250. pynecore/types/type_checker.py +7 -0
  251. pynecore/types/type_checker.pyi +48 -0
  252. pynecore/types/volume_row.py +36 -0
  253. pynecore/types/weekdays.py +11 -0
  254. pynecore/types/xloc.py +5 -0
  255. pynecore/types/yloc.py +5 -0
  256. pynecore/utils/__init__.py +0 -0
  257. pynecore/utils/file_utils.py +50 -0
  258. pynecore/utils/rich/__init__.py +0 -0
  259. pynecore/utils/rich/date_column.py +25 -0
  260. pynecore/utils/sequence_view.py +92 -0
  261. pynecore/utils/stdlib_checker.py +17 -0
@@ -0,0 +1,392 @@
1
+ """
2
+ General-purpose dataclass-based configuration with self-healing TOML files.
3
+
4
+ Generates a TOML configuration file from a Python dataclass definition.
5
+ On each run the file is regenerated from the dataclass: user-modified values
6
+ are preserved while the structure always reflects the current field set.
7
+
8
+ Convention::
9
+
10
+ #key = value — default / unmodified (commented out)
11
+ key = value — user-modified (uncommented)
12
+ #key = — None value
13
+ """
14
+
15
+ import ast
16
+ import dataclasses
17
+ import inspect
18
+ import textwrap
19
+ import tomllib
20
+ from pathlib import Path
21
+ from typing import Any, cast
22
+
23
+
24
+ class mlstr(str):
25
+ """TOML multi-line string marker.
26
+
27
+ Annotate a config field with this type to force the writer to render
28
+ the value as a multi-line literal block (``'''…'''``), even when the
29
+ default is empty or a single line. Useful for fields where users will
30
+ paste multi-line content (PEM keys, certificates, SSH keys, etc.) —
31
+ the commented default already shows the paste markers, so users do
32
+ not have to know TOML's multi-line literal syntax to fill it in.
33
+ """
34
+
35
+
36
+ def format_value(value: str | int | float | bool) -> str:
37
+ """
38
+ Format a Python value as a TOML value string.
39
+
40
+ Handles the four TOML-native types: ``str``, ``int``, ``float``, ``bool``.
41
+ ``mlstr`` values, and any plain string containing ``\\n``, are emitted
42
+ as TOML multi-line literal blocks (``'''…'''``) so PEM keys,
43
+ certificates and similar payloads stay readable in the generated
44
+ config. Strings that contain ``'''`` fall back to a single-line
45
+ escaped form.
46
+ This function is intentionally public so that other modules (e.g.
47
+ ``core.script``) can reuse it for consistent TOML formatting.
48
+
49
+ :param value: The value to format.
50
+ :return: TOML-formatted string representation.
51
+ """
52
+ if isinstance(value, bool):
53
+ return str(value).lower()
54
+ if isinstance(value, int):
55
+ return str(value)
56
+ if isinstance(value, float):
57
+ return str(value)
58
+ if isinstance(value, mlstr) and "'''" not in value:
59
+ return f"'''\n{value}'''"
60
+ if isinstance(value, str):
61
+ if '\n' in value and "'''" not in value:
62
+ return f"'''\n{value}'''"
63
+ escaped = (
64
+ value
65
+ .replace('\\', '\\\\')
66
+ .replace('"', '\\"')
67
+ .replace('\n', '\\n')
68
+ .replace('\r', '\\r')
69
+ )
70
+ return f'"{escaped}"'
71
+ return str(value)
72
+
73
+
74
+ def _format_for_toml(value: Any) -> str:
75
+ """Format any supported value as a TOML literal.
76
+
77
+ Wraps :func:`format_value` for the four scalar types and adds TOML
78
+ inline-table / array support so ``dict`` and ``list`` fields (e.g.
79
+ ``LiveProviderConfig.symbol_map``) round-trip through the
80
+ self-healing config writer.
81
+ """
82
+ if isinstance(value, dict):
83
+ if not value:
84
+ return "{}"
85
+ parts = [
86
+ f"{format_value(str(k))} = {_format_for_toml(v)}"
87
+ for k, v in value.items()
88
+ ]
89
+ return "{ " + ", ".join(parts) + " }"
90
+ if isinstance(value, list):
91
+ if not value:
92
+ return "[]"
93
+ return "[" + ", ".join(_format_for_toml(v) for v in value) + "]"
94
+ return format_value(value)
95
+
96
+
97
+ def _is_mlstr_field(f: dataclasses.Field) -> bool:
98
+ """Check if a dataclass field is annotated as ``mlstr``.
99
+
100
+ Relies on ``f.type`` being a real type object (not a forward-ref
101
+ string) — guaranteed in this project because ``from __future__ import
102
+ annotations`` is intentionally not used.
103
+ """
104
+ t = f.type
105
+ return isinstance(t, type) and issubclass(t, mlstr)
106
+
107
+
108
+ def _emit_assignment(
109
+ lines: list[str],
110
+ name: str,
111
+ formatted: str,
112
+ *,
113
+ commented: bool,
114
+ ) -> None:
115
+ """Append a ``name = value`` assignment to ``lines``.
116
+
117
+ For multi-line formatted values (e.g. ``'''…'''``), every continuation
118
+ line is prefixed with ``#`` when ``commented`` is true so the whole
119
+ block is one TOML comment, not a half-commented stray.
120
+ """
121
+ parts = formatted.split('\n')
122
+ prefix = '#' if commented else ''
123
+ lines.append(f"{prefix}{name} = {parts[0]}")
124
+ for cont in parts[1:]:
125
+ lines.append(f"{prefix}{cont}")
126
+
127
+
128
+ def extract_field_docs(config_cls: type) -> dict[str, str]:
129
+ """
130
+ Extract attribute docstrings from a dataclass source via AST parsing.
131
+
132
+ Looks for ``Expr(Constant(str))`` nodes immediately following
133
+ ``AnnAssign`` nodes in the class body (PEP 257 attribute docstrings).
134
+ Walks the MRO in reverse order so docstrings on inherited fields are
135
+ picked up too — a subclass-level docstring overrides the inherited
136
+ one for the same field name.
137
+
138
+ :param config_cls: The dataclass type to inspect.
139
+ :return: Mapping of field name to its docstring.
140
+ """
141
+ docs: dict[str, str] = {}
142
+ for cls in reversed(config_cls.__mro__):
143
+ if cls is object:
144
+ continue
145
+ try:
146
+ source = textwrap.dedent(inspect.getsource(cls))
147
+ except (OSError, TypeError):
148
+ continue
149
+
150
+ try:
151
+ tree = ast.parse(source)
152
+ except SyntaxError:
153
+ continue
154
+
155
+ class_def = None
156
+ for node in ast.walk(tree):
157
+ if isinstance(node, ast.ClassDef) and node.name == cls.__name__:
158
+ class_def = node
159
+ break
160
+
161
+ if class_def is None:
162
+ continue
163
+
164
+ body = class_def.body
165
+ for i, node in enumerate(body):
166
+ if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
167
+ field_name = node.target.id
168
+ if i + 1 < len(body):
169
+ next_node = body[i + 1]
170
+ if (
171
+ isinstance(next_node, ast.Expr)
172
+ and isinstance(next_node.value, ast.Constant)
173
+ and isinstance(next_node.value.value, str)
174
+ ):
175
+ docs[field_name] = next_node.value.value
176
+
177
+ return docs
178
+
179
+
180
+ def generate_toml(
181
+ config_cls: type,
182
+ user_values: dict | None = None,
183
+ ) -> str:
184
+ """
185
+ Generate a TOML string from a dataclass definition.
186
+
187
+ Fields with user-modified values are written uncommented. Fields at their
188
+ default value are written as comments (``#key = value``).
189
+
190
+ :param config_cls: The config dataclass type.
191
+ :param user_values: User-modified values to write uncommented.
192
+ :return: Generated TOML content string.
193
+ """
194
+ field_docs = extract_field_docs(config_cls)
195
+ lines: list[str] = []
196
+
197
+ class_doc = config_cls.__doc__
198
+ if class_doc:
199
+ for doc_line in class_doc.strip().splitlines():
200
+ stripped = doc_line.strip()
201
+ lines.append(f"# {stripped}" if stripped else "#")
202
+
203
+ for f in dataclasses.fields(cast(Any, config_cls)):
204
+ name = f.name
205
+ default = f.default
206
+ is_ml = _is_mlstr_field(f)
207
+
208
+ # Resolve ``field(default_factory=...)`` to an actual default value
209
+ # so the commented placeholder shows real TOML syntax (e.g.
210
+ # ``#symbol_map = {}``) instead of a bare ``#symbol_map =``.
211
+ if default is dataclasses.MISSING and f.default_factory is not dataclasses.MISSING:
212
+ try:
213
+ default = f.default_factory()
214
+ except Exception: # noqa: BLE001
215
+ default = dataclasses.MISSING
216
+
217
+ lines.append("")
218
+
219
+ if name in field_docs:
220
+ for doc_line in field_docs[name].strip().splitlines():
221
+ lines.append(f"# {doc_line.strip()}")
222
+
223
+ if user_values and name in user_values:
224
+ value = user_values[name]
225
+ if is_ml and not isinstance(value, mlstr):
226
+ value = mlstr(value)
227
+ _emit_assignment(lines, name, _format_for_toml(value), commented=False)
228
+ elif default is dataclasses.MISSING or default is None:
229
+ if is_ml:
230
+ _emit_assignment(lines, name, "'''\n'''", commented=True)
231
+ else:
232
+ lines.append(f"#{name} =")
233
+ else:
234
+ value = default
235
+ if is_ml and not isinstance(value, mlstr):
236
+ value = mlstr(value)
237
+ _emit_assignment(lines, name, _format_for_toml(value), commented=True)
238
+
239
+ return '\n'.join(lines) + '\n'
240
+
241
+
242
+ def parse_toml_with_comments(toml_content: str) -> dict:
243
+ """
244
+ Parse TOML content, returning only uncommented (user-modified) values.
245
+
246
+ Commented lines (``#key = value``) are standard TOML comments and are
247
+ excluded by the parser. Only actively set values are returned.
248
+
249
+ :param toml_content: Raw TOML file content.
250
+ :return: Dict of parsed key-value pairs.
251
+ """
252
+ return tomllib.loads(toml_content)
253
+
254
+
255
+ def ensure_config(config_cls: type, config_path: Path) -> object:
256
+ """
257
+ Main entry point. Call on every application run.
258
+
259
+ 1. If the file does not exist, generate it with all defaults (commented).
260
+ 2. If it exists, read user values, regenerate from the dataclass, write back.
261
+ 3. Return a populated dataclass instance with user values over defaults.
262
+
263
+ The result is cached on ``config_cls._ensured``, so repeated calls
264
+ return the same instance without file I/O.
265
+
266
+ TOML table sections (e.g. ``[binance]``) not managed by the dataclass
267
+ are preserved verbatim at the end of the file.
268
+
269
+ :param config_cls: The config dataclass type (not an instance).
270
+ :param config_path: Path to the TOML file.
271
+ :return: A populated config dataclass instance.
272
+ """
273
+ if hasattr(config_cls, '_ensured'):
274
+ return config_cls._ensured
275
+
276
+ user_values = None
277
+ extra_content = ""
278
+
279
+ if config_path.exists():
280
+ user_values, extra_content = _parse_existing(config_path, config_cls)
281
+
282
+ toml_content = generate_toml(config_cls, user_values)
283
+
284
+ if extra_content:
285
+ toml_content += '\n' + extra_content
286
+ if not extra_content.endswith('\n'):
287
+ toml_content += '\n'
288
+
289
+ config_path.parent.mkdir(parents=True, exist_ok=True)
290
+ config_path.write_text(toml_content, encoding='utf-8')
291
+
292
+ instance = _create_instance(config_cls, user_values)
293
+ config_cls._ensured = instance
294
+ return instance
295
+
296
+
297
+ def _parse_existing(config_path: Path, config_cls: type) -> tuple[dict, str]:
298
+ """
299
+ Parse an existing config file to extract user values and extra sections.
300
+
301
+ :param config_path: Path to the TOML file.
302
+ :param config_cls: The config dataclass type.
303
+ :return: ``(user_values, extra_sections_raw_text)``.
304
+ """
305
+ content = config_path.read_text(encoding='utf-8')
306
+
307
+ parsed = tomllib.loads(content)
308
+
309
+ field_names = {f.name for f in dataclasses.fields(cast(Any, config_cls))}
310
+
311
+ # Field-typed values are kept; non-field keys (typically TOML section
312
+ # headers like ``[binance]``) are left out and preserved verbatim via
313
+ # :func:`_extract_extra_sections`. Dict-typed user values (e.g.
314
+ # ``symbol_map = {...}``) are accepted only when the key matches a
315
+ # known dataclass field.
316
+ user_values: dict = {}
317
+ for key, value in parsed.items():
318
+ if key in field_names:
319
+ user_values[key] = value
320
+
321
+ extra_content = _extract_extra_sections(content, field_names)
322
+
323
+ return user_values, extra_content
324
+
325
+
326
+ def _extract_extra_sections(content: str, field_names: set[str] | None = None) -> str:
327
+ """
328
+ Extract raw text of TOML table sections from file content.
329
+
330
+ Walks the file from top to bottom and keeps every ``[section]`` block
331
+ whose header does not match a known dataclass field. Field-matching
332
+ table sections (e.g. ``[symbol_map]``) are filtered out so the
333
+ self-healing writer does not re-emit them alongside the inline-table
334
+ form already produced by :func:`generate_toml`.
335
+
336
+ :param content: Raw file content.
337
+ :param field_names: Known dataclass field names whose sections should
338
+ be dropped from the extras. ``None`` keeps every
339
+ section (legacy behaviour).
340
+ :return: Raw text of extra sections, or empty string.
341
+ """
342
+ lines = content.splitlines()
343
+ out: list[str] = []
344
+ skipping = False
345
+ for line in lines:
346
+ stripped = line.strip()
347
+ if stripped.startswith('[') and not stripped.startswith('#'):
348
+ # New section header — decide whether to keep or drop.
349
+ header = stripped.strip('[]').split('.', 1)[0]
350
+ skipping = field_names is not None and header in field_names
351
+ if not skipping:
352
+ out.append(line)
353
+ continue
354
+ if not skipping:
355
+ # Only start capturing once we've seen the first kept header.
356
+ if out:
357
+ out.append(line)
358
+ return '\n'.join(out)
359
+
360
+
361
+ def _create_instance(config_cls: type, user_values: dict | None):
362
+ """
363
+ Create a dataclass instance with user values merged over defaults.
364
+
365
+ Handles ``int`` to ``float`` coercion when the field default is a
366
+ float, and ``str`` to ``mlstr`` coercion when the field is annotated
367
+ as ``mlstr`` (tomllib returns plain ``str`` regardless).
368
+
369
+ :param config_cls: The config dataclass type.
370
+ :param user_values: User-modified values, or ``None``.
371
+ :return: A populated config dataclass instance.
372
+ """
373
+ if not user_values:
374
+ return config_cls()
375
+
376
+ kwargs: dict = {}
377
+ for f in dataclasses.fields(cast(Any, config_cls)):
378
+ if f.name not in user_values:
379
+ continue
380
+ value = user_values[f.name]
381
+ if (
382
+ f.default is not dataclasses.MISSING
383
+ and isinstance(f.default, float)
384
+ and isinstance(value, int)
385
+ and not isinstance(value, bool)
386
+ ):
387
+ value = float(value)
388
+ if _is_mlstr_field(f) and isinstance(value, str) and not isinstance(value, mlstr):
389
+ value = mlstr(value)
390
+ kwargs[f.name] = value
391
+
392
+ return config_cls(**kwargs)