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.
- opencode_pyneruntime-6.6.4.dist-info/METADATA +281 -0
- opencode_pyneruntime-6.6.4.dist-info/RECORD +261 -0
- opencode_pyneruntime-6.6.4.dist-info/WHEEL +5 -0
- opencode_pyneruntime-6.6.4.dist-info/entry_points.txt +6 -0
- opencode_pyneruntime-6.6.4.dist-info/licenses/LICENSE +201 -0
- opencode_pyneruntime-6.6.4.dist-info/licenses/NOTICE +21 -0
- opencode_pyneruntime-6.6.4.dist-info/top_level.txt +1 -0
- pynecore/__init__.py +6 -0
- pynecore/cli/__init__.py +2 -0
- pynecore/cli/app.py +238 -0
- pynecore/cli/commands/__init__.py +343 -0
- pynecore/cli/commands/benchmark.py +186 -0
- pynecore/cli/commands/compile.py +198 -0
- pynecore/cli/commands/data.py +857 -0
- pynecore/cli/commands/debug.py +63 -0
- pynecore/cli/commands/optimize.py +956 -0
- pynecore/cli/commands/plugin.py +242 -0
- pynecore/cli/commands/run.py +2006 -0
- pynecore/cli/pluggable.py +132 -0
- pynecore/cli/utils/__init__.py +0 -0
- pynecore/cli/utils/api_error_handler.py +168 -0
- pynecore/cli/utils/broker_picker.py +330 -0
- pynecore/cli/utils/error_hook.py +28 -0
- pynecore/cli/utils/keyreader.py +178 -0
- pynecore/cli/utils/provider_picker.py +19 -0
- pynecore/cli/utils/symbol_browser.py +1149 -0
- pynecore/core/__init__.py +0 -0
- pynecore/core/aggregator.py +257 -0
- pynecore/core/bar_magnifier.py +168 -0
- pynecore/core/broker/__init__.py +64 -0
- pynecore/core/broker/defaults.py +113 -0
- pynecore/core/broker/disappearance.py +927 -0
- pynecore/core/broker/emulator.py +345 -0
- pynecore/core/broker/exceptions.py +346 -0
- pynecore/core/broker/idempotency.py +401 -0
- pynecore/core/broker/intent_builder.py +334 -0
- pynecore/core/broker/journal.py +1785 -0
- pynecore/core/broker/models.py +1600 -0
- pynecore/core/broker/native_failsafe_manager.py +1436 -0
- pynecore/core/broker/one_way_emulator.py +1128 -0
- pynecore/core/broker/position.py +787 -0
- pynecore/core/broker/run_identity.py +126 -0
- pynecore/core/broker/software_entry_stop_engine.py +351 -0
- pynecore/core/broker/software_partial_bracket_engine.py +1379 -0
- pynecore/core/broker/spot_inventory.py +1327 -0
- pynecore/core/broker/storage.py +2655 -0
- pynecore/core/broker/store_helpers.py +2161 -0
- pynecore/core/broker/sync_engine.py +16070 -0
- pynecore/core/broker/validation.py +382 -0
- pynecore/core/class_property.py +7 -0
- pynecore/core/config.py +392 -0
- pynecore/core/csv_file.py +547 -0
- pynecore/core/currency.py +262 -0
- pynecore/core/data_converter.py +1002 -0
- pynecore/core/datetime.py +296 -0
- pynecore/core/download_info.py +71 -0
- pynecore/core/download_runner.py +274 -0
- pynecore/core/htf_aggregator.py +181 -0
- pynecore/core/import_hook.py +358 -0
- pynecore/core/instance_state.py +494 -0
- pynecore/core/live_ltf_collector.py +442 -0
- pynecore/core/live_ltf_window.py +189 -0
- pynecore/core/live_runner.py +1347 -0
- pynecore/core/module_property.py +26 -0
- pynecore/core/ohlcv_file.py +1888 -0
- pynecore/core/overload.py +371 -0
- pynecore/core/pine_cast.py +113 -0
- pynecore/core/pine_export.py +95 -0
- pynecore/core/pine_method.py +244 -0
- pynecore/core/pine_range.py +86 -0
- pynecore/core/pine_udt.py +69 -0
- pynecore/core/plugin/__init__.py +394 -0
- pynecore/core/plugin/broker.py +781 -0
- pynecore/core/plugin/cli.py +96 -0
- pynecore/core/plugin/live_provider.py +208 -0
- pynecore/core/plugin/provider.py +331 -0
- pynecore/core/provider_string.py +148 -0
- pynecore/core/random.py +40 -0
- pynecore/core/resampler.py +686 -0
- pynecore/core/safe_convert.py +64 -0
- pynecore/core/script.py +1011 -0
- pynecore/core/script_runner.py +3202 -0
- pynecore/core/security.py +1749 -0
- pynecore/core/security_process.py +1253 -0
- pynecore/core/security_shm.py +456 -0
- pynecore/core/series.py +417 -0
- pynecore/core/strategy_stats.py +669 -0
- pynecore/core/symbol_map.py +134 -0
- pynecore/core/syminfo.py +505 -0
- pynecore/core/viz.py +591 -0
- pynecore/lib/__init__.py +1771 -0
- pynecore/lib/_fixnan.py +32 -0
- pynecore/lib/_math_stateful.py +202 -0
- pynecore/lib/_timeframe_change.py +101 -0
- pynecore/lib/adjustment.py +6 -0
- pynecore/lib/alert.py +39 -0
- pynecore/lib/alert.pyi +14 -0
- pynecore/lib/array.py +1051 -0
- pynecore/lib/barmerge.py +60 -0
- pynecore/lib/barstate.py +30 -0
- pynecore/lib/box.py +415 -0
- pynecore/lib/chart.py +128 -0
- pynecore/lib/color.py +152 -0
- pynecore/lib/color.pyi +50 -0
- pynecore/lib/currency.py +62 -0
- pynecore/lib/dayofweek.py +36 -0
- pynecore/lib/dayofweek.pyi +18 -0
- pynecore/lib/display.py +8 -0
- pynecore/lib/dividends.py +9 -0
- pynecore/lib/earnings.py +11 -0
- pynecore/lib/extend.py +6 -0
- pynecore/lib/font.py +5 -0
- pynecore/lib/footprint.py +79 -0
- pynecore/lib/format.py +11 -0
- pynecore/lib/hline.py +67 -0
- pynecore/lib/hline.pyi +24 -0
- pynecore/lib/label.py +409 -0
- pynecore/lib/line.py +433 -0
- pynecore/lib/linefill.py +93 -0
- pynecore/lib/location.py +11 -0
- pynecore/lib/log.py +362 -0
- pynecore/lib/map.py +150 -0
- pynecore/lib/math.py +385 -0
- pynecore/lib/matrix.py +708 -0
- pynecore/lib/order.py +8 -0
- pynecore/lib/pivotpointtype.py +8 -0
- pynecore/lib/plot.py +95 -0
- pynecore/lib/plot.pyi +33 -0
- pynecore/lib/polyline.py +91 -0
- pynecore/lib/position.py +15 -0
- pynecore/lib/request.py +281 -0
- pynecore/lib/runtime.py +5 -0
- pynecore/lib/scale.py +9 -0
- pynecore/lib/session.py +267 -0
- pynecore/lib/session.pyi +12 -0
- pynecore/lib/shape.py +18 -0
- pynecore/lib/size.py +12 -0
- pynecore/lib/splits.py +4 -0
- pynecore/lib/strategy/__init__.py +4778 -0
- pynecore/lib/strategy/closedtrades.py +347 -0
- pynecore/lib/strategy/closedtrades.pyi +53 -0
- pynecore/lib/strategy/commission.py +9 -0
- pynecore/lib/strategy/direction.py +9 -0
- pynecore/lib/strategy/oca.py +13 -0
- pynecore/lib/strategy/opentrades.py +281 -0
- pynecore/lib/strategy/opentrades.pyi +49 -0
- pynecore/lib/strategy/risk.py +109 -0
- pynecore/lib/string.py +649 -0
- pynecore/lib/syminfo.py +84 -0
- pynecore/lib/ta.py +2230 -0
- pynecore/lib/table.py +290 -0
- pynecore/lib/text.py +17 -0
- pynecore/lib/ticker.py +207 -0
- pynecore/lib/timeframe.py +293 -0
- pynecore/lib/volume_row.py +67 -0
- pynecore/lib/xloc.py +4 -0
- pynecore/lib/yloc.py +5 -0
- pynecore/providers/__init__.py +0 -0
- pynecore/providers/ccxt.py +664 -0
- pynecore/providers/replay.py +187 -0
- pynecore/pynesys/__init__.py +0 -0
- pynecore/pynesys/api.py +498 -0
- pynecore/pynesys/compiler.py +112 -0
- pynecore/standalone.py +99 -0
- pynecore/testing/__init__.py +1 -0
- pynecore/testing/broker_lab/__init__.py +41 -0
- pynecore/testing/broker_lab/__main__.py +5 -0
- pynecore/testing/broker_lab/cli.py +87 -0
- pynecore/testing/broker_lab/generate.py +47 -0
- pynecore/testing/broker_lab/model.py +84 -0
- pynecore/testing/broker_lab/reference.py +645 -0
- pynecore/testing/broker_lab/runner.py +372 -0
- pynecore/testing/broker_lab/scheduler.py +50 -0
- pynecore/testing/broker_lab/subprocess.py +73 -0
- pynecore/transformers/__init__.py +0 -0
- pynecore/transformers/builtin_shadow.py +136 -0
- pynecore/transformers/closure_arguments_transformer.py +428 -0
- pynecore/transformers/display_rewrite.py +140 -0
- pynecore/transformers/dynamic_default.py +147 -0
- pynecore/transformers/function_isolation.py +757 -0
- pynecore/transformers/import_lifter.py +61 -0
- pynecore/transformers/import_normalizer.py +328 -0
- pynecore/transformers/inline_series_hoist.py +178 -0
- pynecore/transformers/input_transformer.py +175 -0
- pynecore/transformers/lib_series.py +201 -0
- pynecore/transformers/locations.py +70 -0
- pynecore/transformers/module_properties.json +3387 -0
- pynecore/transformers/module_property.py +221 -0
- pynecore/transformers/ne_guard.py +70 -0
- pynecore/transformers/persistent.py +320 -0
- pynecore/transformers/persistent_series.py +76 -0
- pynecore/transformers/safe_convert_transformer.py +97 -0
- pynecore/transformers/safe_division_transformer.py +95 -0
- pynecore/transformers/script_requirements.py +308 -0
- pynecore/transformers/security.py +752 -0
- pynecore/transformers/security_instantiation.py +274 -0
- pynecore/transformers/series.py +275 -0
- pynecore/transformers/slot_layout.py +381 -0
- pynecore/transformers/type_checking_stripper.py +25 -0
- pynecore/transformers/unused_series_detector.py +267 -0
- pynecore/types/__init__.py +21 -0
- pynecore/types/alert.py +5 -0
- pynecore/types/barmerge.py +5 -0
- pynecore/types/base.py +39 -0
- pynecore/types/box.py +37 -0
- pynecore/types/chart.py +17 -0
- pynecore/types/color.py +107 -0
- pynecore/types/currency.py +5 -0
- pynecore/types/datetime.py +6 -0
- pynecore/types/display.py +5 -0
- pynecore/types/dividends.py +5 -0
- pynecore/types/earnings.py +5 -0
- pynecore/types/extend.py +5 -0
- pynecore/types/font.py +5 -0
- pynecore/types/footprint.py +41 -0
- pynecore/types/format.py +5 -0
- pynecore/types/hline.py +24 -0
- pynecore/types/ib_persistent.py +8 -0
- pynecore/types/ib_persistent.pyi +10 -0
- pynecore/types/label.py +35 -0
- pynecore/types/line.py +32 -0
- pynecore/types/linefill.py +13 -0
- pynecore/types/location.py +5 -0
- pynecore/types/matrix.py +999 -0
- pynecore/types/na.py +237 -0
- pynecore/types/na.pyi +83 -0
- pynecore/types/ohlcv.py +12 -0
- pynecore/types/order.py +5 -0
- pynecore/types/persistent.py +8 -0
- pynecore/types/persistent.pyi +13 -0
- pynecore/types/pine_types.py +11 -0
- pynecore/types/pine_types.pyi +15 -0
- pynecore/types/pivotpointtype.py +5 -0
- pynecore/types/plot.py +12 -0
- pynecore/types/plot_meta.py +60 -0
- pynecore/types/polyline.py +40 -0
- pynecore/types/position.py +5 -0
- pynecore/types/scale.py +5 -0
- pynecore/types/script_type.py +15 -0
- pynecore/types/series.py +23 -0
- pynecore/types/series.pyi +19 -0
- pynecore/types/session.py +35 -0
- pynecore/types/shape.py +5 -0
- pynecore/types/size.py +5 -0
- pynecore/types/source.py +33 -0
- pynecore/types/splits.py +5 -0
- pynecore/types/strategy.py +45 -0
- pynecore/types/table.py +87 -0
- pynecore/types/text.py +13 -0
- pynecore/types/type_checker.py +7 -0
- pynecore/types/type_checker.pyi +48 -0
- pynecore/types/volume_row.py +36 -0
- pynecore/types/weekdays.py +11 -0
- pynecore/types/xloc.py +5 -0
- pynecore/types/yloc.py +5 -0
- pynecore/utils/__init__.py +0 -0
- pynecore/utils/file_utils.py +50 -0
- pynecore/utils/rich/__init__.py +0 -0
- pynecore/utils/rich/date_column.py +25 -0
- pynecore/utils/sequence_view.py +92 -0
- 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()
|