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
pynecore/core/viz.py ADDED
@@ -0,0 +1,591 @@
1
+ """
2
+ Plot-style and drawing visual-data serialization for PyneCore.
3
+
4
+ Turns the per-run plot-family metadata (``lib._plot_meta``), per-bar plot values
5
+ and dynamic color channels (``lib._plot_data`` / ``lib._viz_dyn``) and the live
6
+ drawing registries (line/label/box/table/polyline/linefill) into a compact
7
+ NDJSON stream. One JSON object per line; record kinds are tagged by ``"t"``:
8
+ ``hdr`` (header), ``meta`` (a plot-family definition), ``bar`` (per-bar values +
9
+ changed colors), ``ev`` (a drawing create/update/delete event when journaling),
10
+ ``drawings`` (a full end-of-run snapshot) and ``end``.
11
+
12
+ Enum reverse maps are built once, at import time, by introspecting the ``lib.*``
13
+ constant modules, so there is no hand-maintained name table to drift.
14
+ """
15
+ import json
16
+ import math
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+ from pynecore import lib
21
+ from pynecore.lib import (
22
+ plot as _plot_mod, display as _display_mod, shape as _shape_mod,
23
+ location as _location_mod, size as _size_mod, xloc as _xloc_mod,
24
+ yloc as _yloc_mod, extend as _extend_mod, position as _position_mod,
25
+ line as _line_mod, label as _label_mod, hline as _hline_mod,
26
+ text as _text_mod, font as _font_mod, format as _format_mod,
27
+ box as _box_mod, table as _table_mod, polyline as _polyline_mod,
28
+ linefill as _linefill_mod,
29
+ )
30
+ from pynecore.types import script_type as _script_type_mod
31
+ from pynecore.types.base import reset_vid_counter
32
+ from pynecore.types.color import Color
33
+ from pynecore.types.na import NA
34
+ from pynecore.types.plot import PlotEnum
35
+ from pynecore.types.display import Display
36
+ from pynecore.types.script_type import ScriptType
37
+ from pynecore.types.shape import Shape
38
+ from pynecore.types.location import Location
39
+ from pynecore.types.size import Size
40
+ from pynecore.types.xloc import XLoc
41
+ from pynecore.types.yloc import YLoc
42
+ from pynecore.types.extend import Extend
43
+ from pynecore.types.position import Position
44
+ from pynecore.types.line import LineEnum
45
+ from pynecore.types.label import LabelStyleEnum
46
+ from pynecore.types.hline import HLineEnum
47
+ from pynecore.types.text import AlignEnum, FormatEnum, WrapEnum
48
+ from pynecore.types.font import FontFamilyEnum
49
+ from pynecore.types.format import Format
50
+
51
+
52
+ #
53
+ # Enum name maps — built once by module introspection
54
+ #
55
+
56
+ def _names(mod, cls, strip: str = '') -> dict:
57
+ """Reverse-map every ``cls`` constant in ``mod`` to its attribute name.
58
+
59
+ :param mod: The module holding the constants (e.g. ``lib.shape``)
60
+ :param cls: The constant type to collect (e.g. ``Shape``)
61
+ :param strip: Optional attribute-name prefix to drop (e.g. ``'style_'``)
62
+ :return: ``{constant_value: attr_name}``. StrLiteral values compare equal to
63
+ their raw string, so a single entry serves both object and str lookups.
64
+ """
65
+ return {v: (k[len(strip):] if strip and k.startswith(strip) else k)
66
+ for k, v in vars(mod).items() if isinstance(v, cls)}
67
+
68
+
69
+ PLOT_STYLE = _names(_plot_mod, PlotEnum, strip='style_')
70
+ PLOT_LINESTYLE = _names(_plot_mod, PlotEnum, strip='linestyle_')
71
+ DISPLAY = _names(_display_mod, Display)
72
+ SCRIPT_TYPE = _names(_script_type_mod, ScriptType)
73
+ SHAPE = _names(_shape_mod, Shape)
74
+ LOCATION = _names(_location_mod, Location)
75
+ SIZE = _names(_size_mod, Size)
76
+ XLOC = _names(_xloc_mod, XLoc)
77
+ YLOC = _names(_yloc_mod, YLoc)
78
+ EXTEND = _names(_extend_mod, Extend)
79
+ POSITION = _names(_position_mod, Position)
80
+ LINE_STYLE = _names(_line_mod, LineEnum, strip='style_')
81
+ LABEL_STYLE = _names(_label_mod, LabelStyleEnum, strip='style_')
82
+ HLINE_LINESTYLE = _names(_hline_mod, HLineEnum, strip='style_')
83
+ ALIGN = _names(_text_mod, AlignEnum, strip='align_')
84
+ FORMAT = _names(_text_mod, FormatEnum, strip='format_')
85
+ WRAP = _names(_text_mod, WrapEnum, strip='wrap_')
86
+ FONT = _names(_font_mod, FontFamilyEnum, strip='family_')
87
+ NUM_FORMAT = _names(_format_mod, Format)
88
+
89
+
90
+ def enum_name(value: Any, table: dict, default: Any = None) -> Any:
91
+ """Look up ``value`` in an enum reverse map.
92
+
93
+ :param value: The enum constant (or its raw str/int)
94
+ :param table: A reverse map built by :func:`_names`
95
+ :param default: Returned when ``value`` is ``None`` / unknown int enum
96
+ :return: The attribute name, or the raw string for an unknown StrLiteral,
97
+ or ``default``
98
+ """
99
+ if value is None:
100
+ return default
101
+ try:
102
+ if value in table:
103
+ return table[value]
104
+ except TypeError:
105
+ pass
106
+ if isinstance(value, str):
107
+ return str(value)
108
+ return default
109
+
110
+
111
+ #
112
+ # Value serialization helpers
113
+ #
114
+
115
+ def color_str(c: Any) -> str | None:
116
+ """Serialize a :class:`Color` to ``#RRGGBBAA``; ``None`` / NA -> ``None``."""
117
+ if c is None or isinstance(c, NA):
118
+ return None
119
+ return f'#{c.value:08X}'
120
+
121
+
122
+ def _coord(x: Any) -> Any:
123
+ """Drawing coordinate: NA -> ``None``, everything else unchanged."""
124
+ return None if isinstance(x, NA) else x
125
+
126
+
127
+ def _size_val(v: Any) -> Any:
128
+ """Cell/text size: a ``Size`` constant -> its name, an int -> the int."""
129
+ if v is None:
130
+ return None
131
+ if isinstance(v, str):
132
+ return enum_name(v, SIZE, str(v))
133
+ return v
134
+
135
+
136
+ def json_default(o: Any) -> Any:
137
+ """``json.dumps`` fallback: NA -> ``None``, Color -> hex, else raise."""
138
+ if isinstance(o, NA):
139
+ return None
140
+ if isinstance(o, Color):
141
+ return f'#{o.value:08X}'
142
+ raise TypeError(f"Object of type {type(o).__name__} is not JSON serializable")
143
+
144
+
145
+ def _scrub(o: Any) -> Any:
146
+ """Replace non-finite floats with ``None`` recursively (json ``allow_nan=False``)."""
147
+ if isinstance(o, float):
148
+ return o if math.isfinite(o) else None
149
+ if isinstance(o, dict):
150
+ return {k: _scrub(v) for k, v in o.items()}
151
+ if isinstance(o, list):
152
+ return [_scrub(v) for v in o]
153
+ return o
154
+
155
+
156
+ def _clean(d: dict) -> dict:
157
+ """Drop keys whose value is ``None``."""
158
+ return {k: v for k, v in d.items() if v is not None}
159
+
160
+
161
+ #
162
+ # Plot-family metadata
163
+ #
164
+
165
+ def serialize_meta(meta: Any) -> dict:
166
+ """Serialize a :class:`PlotMeta` to a ``meta`` record.
167
+
168
+ Enum fields are mapped to names, colors to hex, kind-specific defaults are
169
+ applied when the source field is ``None``, and remaining ``None`` fields are
170
+ dropped.
171
+
172
+ :param meta: The registered plot-family metadata object
173
+ :return: A JSON-ready ``{"t":"meta", ...}`` dict
174
+ """
175
+ k = meta.kind
176
+ d: dict[str, Any] = {"t": "meta", "id": meta.id, "kind": k}
177
+
178
+ if meta.title is not None:
179
+ d["title"] = meta.title
180
+ d["display"] = enum_name(meta.display, DISPLAY, "all")
181
+ if meta.force_overlay:
182
+ d["force_overlay"] = True
183
+ if not meta.editable:
184
+ d["editable"] = False
185
+ if meta.offset:
186
+ d["offset"] = meta.offset
187
+ if meta.show_last is not None:
188
+ d["show_last"] = meta.show_last
189
+ if meta.format is not None:
190
+ d["format"] = meta.format
191
+ if meta.precision is not None:
192
+ d["precision"] = meta.precision
193
+
194
+ if k == 'plot':
195
+ d["style"] = enum_name(meta.style, PLOT_STYLE, "line")
196
+ d["color"] = color_str(meta.color)
197
+ d["linewidth"] = meta.linewidth
198
+ if meta.trackprice:
199
+ d["trackprice"] = True
200
+ if meta.histbase:
201
+ d["histbase"] = meta.histbase
202
+ if meta.join:
203
+ d["join"] = True
204
+ elif k == 'shape':
205
+ d["style"] = enum_name(meta.style, SHAPE, "xcross")
206
+ d["location"] = enum_name(meta.location, LOCATION, "abovebar")
207
+ d["size"] = enum_name(meta.size, SIZE, "auto")
208
+ d["color"] = color_str(meta.color)
209
+ if meta.text is not None:
210
+ d["text"] = meta.text
211
+ d["textcolor"] = color_str(meta.textcolor)
212
+ elif k == 'char':
213
+ d["char"] = meta.char if meta.char is not None else "◆"
214
+ d["location"] = enum_name(meta.location, LOCATION, "abovebar")
215
+ d["size"] = enum_name(meta.size, SIZE, "auto")
216
+ d["color"] = color_str(meta.color)
217
+ if meta.text is not None:
218
+ d["text"] = meta.text
219
+ d["textcolor"] = color_str(meta.textcolor)
220
+ elif k == 'arrow':
221
+ d["colorup"] = color_str(meta.colorup)
222
+ d["colordown"] = color_str(meta.colordown)
223
+ if meta.minheight is not None:
224
+ d["minheight"] = meta.minheight
225
+ if meta.maxheight is not None:
226
+ d["maxheight"] = meta.maxheight
227
+ elif k == 'candle':
228
+ d["color"] = color_str(meta.color)
229
+ d["wickcolor"] = color_str(meta.wickcolor)
230
+ d["bordercolor"] = color_str(meta.bordercolor)
231
+ elif k == 'bar':
232
+ d["color"] = color_str(meta.color)
233
+ elif k == 'hline':
234
+ d["price"] = meta.price
235
+ d["color"] = color_str(meta.color)
236
+ d["linewidth"] = meta.linewidth
237
+ d["linestyle"] = enum_name(meta.linestyle, HLINE_LINESTYLE, "solid")
238
+ elif k == 'fill':
239
+ d["color"] = color_str(meta.color)
240
+ if meta.plot1 is not None:
241
+ d["plot1"] = meta.plot1
242
+ if meta.plot2 is not None:
243
+ d["plot2"] = meta.plot2
244
+ if meta.hline1 is not None:
245
+ d["hline1"] = meta.hline1
246
+ if meta.hline2 is not None:
247
+ d["hline2"] = meta.hline2
248
+ if meta.fillgaps:
249
+ d["fillgaps"] = True
250
+
251
+ if meta.dynamic:
252
+ d["dynamic"] = True
253
+
254
+ return _clean(d)
255
+
256
+
257
+ #
258
+ # Drawing serialization
259
+ #
260
+
261
+ def _line_dict(l: Any) -> dict:
262
+ return {
263
+ "id": l.vid,
264
+ "x1": _coord(l.x1), "y1": _coord(l.y1),
265
+ "x2": _coord(l.x2), "y2": _coord(l.y2),
266
+ "xloc": enum_name(l.xloc, XLOC),
267
+ "extend": enum_name(l.extend, EXTEND),
268
+ "color": color_str(l.color),
269
+ "style": enum_name(l.style, LINE_STYLE),
270
+ "width": l.width,
271
+ "force_overlay": l.force_overlay,
272
+ }
273
+
274
+
275
+ def _label_dict(la: Any) -> dict:
276
+ return {
277
+ "id": la.vid,
278
+ "x": _coord(la.x), "y": _coord(la.y),
279
+ "text": la.text,
280
+ "xloc": enum_name(la.xloc, XLOC),
281
+ "yloc": enum_name(la.yloc, YLOC),
282
+ "color": color_str(la.color),
283
+ "style": enum_name(la.style, LABEL_STYLE),
284
+ "textcolor": color_str(la.textcolor),
285
+ "size": enum_name(la.size, SIZE),
286
+ "textalign": enum_name(la.textalign, ALIGN),
287
+ "tooltip": la.tooltip,
288
+ "text_font_family": enum_name(la.text_font_family, FONT),
289
+ "force_overlay": la.force_overlay,
290
+ "text_formatting": enum_name(la.text_formatting, FORMAT),
291
+ }
292
+
293
+
294
+ def _box_dict(b: Any) -> dict:
295
+ return {
296
+ "id": b.vid,
297
+ "left": _coord(b.left), "top": _coord(b.top),
298
+ "right": _coord(b.right), "bottom": _coord(b.bottom),
299
+ "border_color": color_str(b.border_color),
300
+ "border_width": b.border_width,
301
+ "border_style": enum_name(b.border_style, LINE_STYLE),
302
+ "extend": enum_name(b.extend, EXTEND),
303
+ "xloc": enum_name(b.xloc, XLOC),
304
+ "bgcolor": color_str(b.bgcolor),
305
+ "text": b.text,
306
+ "text_size": _size_val(b.text_size),
307
+ "text_color": color_str(b.text_color),
308
+ "text_halign": enum_name(b.text_halign, ALIGN),
309
+ "text_valign": enum_name(b.text_valign, ALIGN),
310
+ "text_wrap": enum_name(b.text_wrap, WRAP),
311
+ "text_font_family": enum_name(b.text_font_family, FONT),
312
+ "text_formatting": enum_name(b.text_formatting, FORMAT),
313
+ "force_overlay": b.force_overlay,
314
+ }
315
+
316
+
317
+ def _cell_dict(col: int, row: int, cell: Any) -> dict:
318
+ d = {
319
+ "col": col, "row": row,
320
+ "text": cell.text,
321
+ "width": cell.width,
322
+ "height": cell.height,
323
+ "text_color": color_str(cell.text_color),
324
+ "text_halign": enum_name(cell.text_halign, ALIGN),
325
+ "text_valign": enum_name(cell.text_valign, ALIGN),
326
+ "text_size": _size_val(cell.text_size),
327
+ "bgcolor": color_str(cell.bgcolor),
328
+ "tooltip": cell.tooltip,
329
+ "text_font_family": enum_name(cell.text_font_family, FONT),
330
+ "text_formatting": enum_name(cell.text_formatting, FORMAT),
331
+ }
332
+ if cell.is_merged:
333
+ # [start_col, start_row, end_col, end_row] of the merged range; the range's
334
+ # top-left cell renders, the others are hidden by it.
335
+ d["merge"] = [cell.merge_start_col, cell.merge_start_row,
336
+ cell.merge_end_col, cell.merge_end_row]
337
+ return d
338
+
339
+
340
+ def _table_dict(t: Any) -> dict:
341
+ return {
342
+ "id": t.vid,
343
+ "position": enum_name(t.position, POSITION),
344
+ "columns": t.columns,
345
+ "rows": t.rows,
346
+ "bgcolor": color_str(t.bgcolor),
347
+ "frame_color": color_str(t.frame_color),
348
+ "frame_width": t.frame_width,
349
+ "border_color": color_str(t.border_color),
350
+ "border_width": t.border_width,
351
+ "force_overlay": t.force_overlay,
352
+ "cells": [_cell_dict(col, row, cell) for (col, row), cell in t.cells.items()],
353
+ }
354
+
355
+
356
+ def _point_dict(p: Any) -> dict:
357
+ return {"index": _coord(p.index), "time": _coord(p.time), "price": _coord(p.price)}
358
+
359
+
360
+ def _polyline_dict(p: Any) -> dict:
361
+ return {
362
+ "id": p.vid,
363
+ "points": [_point_dict(pt) for pt in p.points],
364
+ "curved": p.curved,
365
+ "closed": p.closed,
366
+ "xloc": enum_name(p.xloc, XLOC),
367
+ "line_color": color_str(p.line_color),
368
+ "fill_color": color_str(p.fill_color),
369
+ "line_style": enum_name(p.line_style, LINE_STYLE),
370
+ "line_width": p.line_width,
371
+ "force_overlay": p.force_overlay,
372
+ }
373
+
374
+
375
+ def _linefill_dict(lf: Any) -> dict:
376
+ return {
377
+ "id": lf.vid,
378
+ "line1": lf.line1.vid,
379
+ "line2": lf.line2.vid,
380
+ "color": color_str(lf.color),
381
+ "line1_state": _line_dict(lf.line1),
382
+ "line2_state": _line_dict(lf.line2),
383
+ }
384
+
385
+
386
+ def serialize_drawing(obj: Any) -> dict:
387
+ """Serialize a single drawing object by its type."""
388
+ from pynecore.types.line import Line
389
+ from pynecore.types.label import Label
390
+ from pynecore.types.box import Box
391
+ from pynecore.types.table import Table
392
+ from pynecore.types.polyline import Polyline
393
+ from pynecore.types.linefill import LineFill
394
+ if isinstance(obj, Line):
395
+ return _line_dict(obj)
396
+ if isinstance(obj, Label):
397
+ return _label_dict(obj)
398
+ if isinstance(obj, Box):
399
+ return _box_dict(obj)
400
+ if isinstance(obj, Table):
401
+ return _table_dict(obj)
402
+ if isinstance(obj, Polyline):
403
+ return _polyline_dict(obj)
404
+ if isinstance(obj, LineFill):
405
+ return _linefill_dict(obj)
406
+ raise TypeError(f"Not a drawing object: {type(obj).__name__}")
407
+
408
+
409
+ def _all_drawings() -> dict:
410
+ """``{(family, vid): serialized_state}`` for every live drawing."""
411
+ out: dict[tuple[str, int], dict] = {}
412
+ for l in list(_line_mod._registry):
413
+ out[("line", l.vid)] = _line_dict(l)
414
+ for la in list(_label_mod._registry):
415
+ out[("label", la.vid)] = _label_dict(la)
416
+ for b in list(_box_mod._registry):
417
+ out[("box", b.vid)] = _box_dict(b)
418
+ for t in list(_table_mod._registry):
419
+ out[("table", t.vid)] = _table_dict(t)
420
+ for p in list(_polyline_mod._registry):
421
+ out[("polyline", p.vid)] = _polyline_dict(p)
422
+ for lf in list(_linefill_mod._registry):
423
+ out[("linefill", lf.vid)] = _linefill_dict(lf)
424
+ return out
425
+
426
+
427
+ def drawings_snapshot() -> dict:
428
+ """Full snapshot of every live drawing, grouped by family."""
429
+ snap: dict[str, Any] = {
430
+ "t": "drawings", "lines": [], "labels": [], "boxes": [],
431
+ "tables": [], "polylines": [], "linefills": [],
432
+ }
433
+ for l in list(_line_mod._registry):
434
+ snap["lines"].append(_line_dict(l))
435
+ for la in list(_label_mod._registry):
436
+ snap["labels"].append(_label_dict(la))
437
+ for b in list(_box_mod._registry):
438
+ snap["boxes"].append(_box_dict(b))
439
+ for t in list(_table_mod._registry):
440
+ snap["tables"].append(_table_dict(t))
441
+ for p in list(_polyline_mod._registry):
442
+ snap["polylines"].append(_polyline_dict(p))
443
+ for lf in list(_linefill_mod._registry):
444
+ snap["linefills"].append(_linefill_dict(lf))
445
+ return snap
446
+
447
+
448
+ def journal_diff(shadow: dict, bar_index: int) -> list[dict]:
449
+ """Diff the live drawings against ``shadow`` and update it in place.
450
+
451
+ :param shadow: ``{(family, vid): state}`` from the previous bar; mutated here
452
+ :param bar_index: The current bar index, recorded on every event
453
+ :return: A list of ``create`` / ``update`` / ``delete`` event records
454
+ """
455
+ current = _all_drawings()
456
+ events: list[dict] = []
457
+ for key, state in current.items():
458
+ old = shadow.get(key)
459
+ if old is None:
460
+ events.append({"t": "ev", "i": bar_index, "op": "create",
461
+ "obj": key[0], "id": key[1], "s": state})
462
+ elif old != state:
463
+ events.append({"t": "ev", "i": bar_index, "op": "update",
464
+ "obj": key[0], "id": key[1], "s": state})
465
+ for key in shadow:
466
+ if key not in current:
467
+ events.append({"t": "ev", "i": bar_index, "op": "delete",
468
+ "obj": key[0], "id": key[1]})
469
+ shadow.clear()
470
+ shadow.update(current)
471
+ return events
472
+
473
+
474
+ def reset_state() -> None:
475
+ """Clear all plot-family and drawing state at the start of a run."""
476
+ lib._plot_meta.clear()
477
+ lib._plot_meta_new.clear()
478
+ lib._viz_dyn.clear()
479
+ lib._viz_seq.clear()
480
+ _line_mod._registry.clear()
481
+ _label_mod._registry.clear()
482
+ _box_mod._registry.clear()
483
+ _table_mod._registry.clear()
484
+ _polyline_mod._registry.clear()
485
+ _linefill_mod._registry.clear()
486
+ reset_vid_counter()
487
+
488
+
489
+ def _encode_color_channel(v: Any) -> Any:
490
+ """Encode a dynamic color channel: Color -> hex, tuple -> list, NA -> None."""
491
+ if v is None or isinstance(v, NA):
492
+ return None
493
+ if isinstance(v, Color):
494
+ return color_str(v)
495
+ if isinstance(v, tuple):
496
+ return [_encode_color_channel(x) for x in v]
497
+ return v
498
+
499
+
500
+ class VizWriter:
501
+ """Buffered main-thread NDJSON writer for plot / drawing visual data."""
502
+
503
+ def __init__(self, path: Path):
504
+ self.path = Path(path)
505
+ self._f: Any = None
506
+ self._last_colors: dict[str, Any] = {}
507
+ self.bars = 0
508
+
509
+ def open(self) -> None:
510
+ self._last_colors.clear()
511
+ self.bars = 0
512
+ self._f = open(self.path, 'w', buffering=1 << 16)
513
+
514
+ def close(self) -> None:
515
+ if self._f is not None:
516
+ self._f.close()
517
+ self._f = None
518
+
519
+ @property
520
+ def is_open(self) -> bool:
521
+ return self._f is not None
522
+
523
+ def _emit(self, obj: dict) -> None:
524
+ self._f.write(json.dumps(_scrub(obj), separators=(',', ':'),
525
+ default=json_default, allow_nan=False))
526
+ self._f.write('\n')
527
+
528
+ def write_header(self, script: Any, syminfo: Any, journal: bool) -> None:
529
+ """Write the ``hdr`` record with script + symbol context."""
530
+ script_rec = _clean({
531
+ "type": enum_name(script.script_type, SCRIPT_TYPE),
532
+ "title": script.title,
533
+ "shorttitle": script.shorttitle,
534
+ "overlay": script.overlay,
535
+ "format": enum_name(script.format, NUM_FORMAT),
536
+ "precision": script.precision,
537
+ "explicit_plot_zorder": script.explicit_plot_zorder,
538
+ "max_lines_count": script.max_lines_count,
539
+ "max_labels_count": script.max_labels_count,
540
+ "max_boxes_count": script.max_boxes_count,
541
+ "max_polylines_count": script.max_polylines_count,
542
+ })
543
+ syminfo_rec = _clean({
544
+ "tickerid": getattr(syminfo, 'tickerid', None) or None,
545
+ "prefix": getattr(syminfo, 'prefix', None) or None,
546
+ "ticker": getattr(syminfo, 'ticker', None) or None,
547
+ "timeframe": getattr(syminfo, 'period', None) or None,
548
+ "mintick": getattr(syminfo, 'mintick', None) or None,
549
+ "timezone": getattr(syminfo, 'timezone', None) or None,
550
+ "type": getattr(syminfo, 'type', None) or None,
551
+ })
552
+ self._emit({"t": "hdr", "v": 1, "script": script_rec,
553
+ "syminfo": syminfo_rec, "journal": journal})
554
+
555
+ def write_bar(self, bar_index: int, time_ms: int, values: dict, dyn: dict) -> None:
556
+ """Drain pending metas, then write one ``bar`` record.
557
+
558
+ ``"v"`` holds the per-bar plot values, ``"c"`` the color channels that
559
+ changed since the last emitted value. Both keys are omitted when empty.
560
+ """
561
+ if lib._plot_meta_new:
562
+ for meta in lib._plot_meta_new:
563
+ self._emit(serialize_meta(meta))
564
+ lib._plot_meta_new.clear()
565
+
566
+ rec: dict[str, Any] = {"t": "bar", "i": bar_index, "time": time_ms}
567
+ if values:
568
+ rec["v"] = dict(values)
569
+ c: dict[str, Any] = {}
570
+ for cid, val in dyn.items():
571
+ enc = _encode_color_channel(val)
572
+ if self._last_colors.get(cid, _MISSING) != enc:
573
+ c[cid] = enc
574
+ self._last_colors[cid] = enc
575
+ if c:
576
+ rec["c"] = c
577
+ self._emit(rec)
578
+ self.bars += 1
579
+
580
+ def write_events(self, events: list[dict]) -> None:
581
+ for ev in events:
582
+ self._emit(ev)
583
+
584
+ def write_drawings_snapshot(self) -> None:
585
+ self._emit(drawings_snapshot())
586
+
587
+ def write_end(self, bars: int) -> None:
588
+ self._emit({"t": "end", "bars": bars})
589
+
590
+
591
+ _MISSING = object()