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
|
@@ -0,0 +1,1749 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Runtime protocol for request.security() — chart-side and security-side functions.
|
|
3
|
+
|
|
4
|
+
The SecurityTransformer (Phase 1) rewrites request.security() calls into four protocol
|
|
5
|
+
functions: __sec_signal__, __sec_write__, __sec_read__, __sec_wait__. This module provides
|
|
6
|
+
the runtime implementations that coordinate via shared memory and multiprocessing Events.
|
|
7
|
+
|
|
8
|
+
Architecture:
|
|
9
|
+
- Chart process: signals security processes, waits for results, reads from shared memory
|
|
10
|
+
- Security process: receives signals, runs bars, writes results to shared memory
|
|
11
|
+
- Cross-context reads: security processes read other contexts' latest values immediately
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
import threading
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from datetime import date, datetime, time, timedelta
|
|
19
|
+
from enum import Enum, auto
|
|
20
|
+
from multiprocessing import Event, Lock, connection
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import TYPE_CHECKING
|
|
23
|
+
from zoneinfo import ZoneInfo
|
|
24
|
+
|
|
25
|
+
from .datetime import parse_timezone
|
|
26
|
+
from .security_shm import (
|
|
27
|
+
SyncBlock, ResultBlock, ResultReader, INITIAL_RESULT_SIZE,
|
|
28
|
+
FLAG_IS_DEVELOPING, FLAG_CLOSED_OVERRIDE,
|
|
29
|
+
FLAG_LTF_WINDOW, FLAG_LTF_CHART_DEVELOPING, FLAG_LTF_LIVE_PHASE,
|
|
30
|
+
write_result,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from multiprocessing.process import BaseProcess
|
|
35
|
+
from multiprocessing.synchronize import Event as EventType, Lock as LockType
|
|
36
|
+
from typing import Callable
|
|
37
|
+
from .resampler import Resampler
|
|
38
|
+
from .htf_aggregator import HTFAggregator
|
|
39
|
+
from .syminfo import SymInfo, SymInfoSession, SymInfoInterval
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Lookahead(Enum):
|
|
45
|
+
"""Lookahead mode for a security context.
|
|
46
|
+
|
|
47
|
+
OFF
|
|
48
|
+
TV-faithful default. The security context advances to the most
|
|
49
|
+
recent HTF bar that has CLOSED at or before the chart bar's CLOSE
|
|
50
|
+
instant — the HTF period's last chart bar already carries the
|
|
51
|
+
period's final value. In historical mode this matches TradingView's
|
|
52
|
+
``barmerge.lookahead_off`` exactly. In live mode the chart-side
|
|
53
|
+
``HTFAggregator`` ships each freshly closed HTF bar to the
|
|
54
|
+
subprocess via the SyncBlock (the static ``.ohlcv`` file cannot
|
|
55
|
+
grow at runtime) as soon as the confirmed chart bar completing the
|
|
56
|
+
period is folded — no developing-bar exposure.
|
|
57
|
+
|
|
58
|
+
LAST_CLOSED
|
|
59
|
+
PyneSys-native, repaint-free alternative. Always returns the most
|
|
60
|
+
recently closed security bar. In historical mode it is functionally
|
|
61
|
+
equivalent to ``OFF``; in live mode it uses the same closed-bar
|
|
62
|
+
transport as ``OFF`` (no developing exposure) and remains
|
|
63
|
+
repaint-free. Recommended for non-charting backtests when the TV
|
|
64
|
+
``close[1]`` idiom is not desired.
|
|
65
|
+
|
|
66
|
+
ON
|
|
67
|
+
TV ``lookahead_on`` semantics — the security context steps into
|
|
68
|
+
the bar that *contains* the chart bar's time. In live mode the
|
|
69
|
+
bar runs with ``barstate.isconfirmed=False`` and OHLCV aggregated
|
|
70
|
+
from the chart timeframe by ``HTFAggregator``; on HTF period
|
|
71
|
+
boundaries the closed bar is delivered first (snapshot saved),
|
|
72
|
+
then the fresh developing bar. In historical/backtest mode the
|
|
73
|
+
containing period's bar is already complete in the child's data
|
|
74
|
+
file, so the child reads its final OHLCV: a bare ``close`` on the
|
|
75
|
+
developing period reproduces TV's classical historical future-leak,
|
|
76
|
+
and the inner-``[1]`` daily-pivot idiom
|
|
77
|
+
``request.security(sym, "D", close[1], lookahead_on)`` reads the
|
|
78
|
+
just-closed prior period (yesterday) — matching TradingView. Use
|
|
79
|
+
``LAST_CLOSED`` for the repaint-free alternative. The outer-``[1]``
|
|
80
|
+
form ``request.security(..., close, lookahead_on)[1]`` is also
|
|
81
|
+
TV-compatible because ``close[1]`` on the chart series is the
|
|
82
|
+
previously delivered value.
|
|
83
|
+
|
|
84
|
+
**Cross-symbol HTF** — when the security symbol differs from the
|
|
85
|
+
chart symbol there is no chart-side aggregator (the chart OHLCV
|
|
86
|
+
is the wrong instrument). The chart-side read returns ``na`` while
|
|
87
|
+
an HTF period is open (``na_on_developing``); at the period
|
|
88
|
+
boundary the chart receives the just-closed cross-symbol HTF
|
|
89
|
+
close, and the TV ``request.security(..., lookahead_on)[1]``
|
|
90
|
+
idiom continues to deliver that value on the next chart bar.
|
|
91
|
+
Behaviour is identical in historical and live mode.
|
|
92
|
+
"""
|
|
93
|
+
OFF = auto()
|
|
94
|
+
LAST_CLOSED = auto()
|
|
95
|
+
ON = auto()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _lookahead_mode(value) -> Lookahead:
|
|
99
|
+
"""Map a ``barmerge.lookahead_*`` singleton (or None) to a :class:`Lookahead`."""
|
|
100
|
+
from pynecore.lib import barmerge
|
|
101
|
+
if value is barmerge.lookahead_on:
|
|
102
|
+
return Lookahead.ON
|
|
103
|
+
if value is barmerge.lookahead_last_closed:
|
|
104
|
+
return Lookahead.LAST_CLOSED
|
|
105
|
+
return Lookahead.OFF
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# Liveness poll interval for security-process waits without a death watcher
|
|
109
|
+
# (legacy fallback). Short enough to detect a crashed child quickly.
|
|
110
|
+
_LIVENESS_POLL_SECONDS = 0.5
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def watch_security_child(
|
|
114
|
+
sec_id: str,
|
|
115
|
+
proc: 'BaseProcess',
|
|
116
|
+
failed_children: set[str],
|
|
117
|
+
events: 'tuple[EventType, ...]',
|
|
118
|
+
) -> None:
|
|
119
|
+
"""
|
|
120
|
+
Start a daemon thread that watches a security child for abnormal death.
|
|
121
|
+
|
|
122
|
+
The per-bar chart waits must be UNTIMED ``event.wait()`` calls: macOS has
|
|
123
|
+
no ``sem_timedwait``, so a timed multiprocessing wait falls back to
|
|
124
|
+
CPython's ``sem_timedwait_save`` emulation — a ``sem_trywait`` +
|
|
125
|
+
``select()`` polling loop with millisecond-growing sleeps that adds up to
|
|
126
|
+
~20ms of wake latency PER WAIT regardless of the timeout value. On a per-
|
|
127
|
+
bar signalled context that quantization dominated the whole run (measured
|
|
128
|
+
~2ms/bar, >80% of wall time). Death detection therefore moves out of the
|
|
129
|
+
wait: this watcher blocks on the process sentinel (no polling), and on a
|
|
130
|
+
non-zero exit registers the sec_id in ``failed_children`` BEFORE setting
|
|
131
|
+
the events, so a blocked ``_wait_with_liveness`` wakes immediately and
|
|
132
|
+
raises instead of deadlocking. A clean exit (code 0) registers nothing —
|
|
133
|
+
a child never exits cleanly while the chart still waits on it.
|
|
134
|
+
|
|
135
|
+
:param sec_id: Security context id the process serves
|
|
136
|
+
:param proc: The started child process
|
|
137
|
+
:param failed_children: Shared registry of abnormally died sec_ids
|
|
138
|
+
:param events: Events a chart wait may block on for this context
|
|
139
|
+
"""
|
|
140
|
+
def _watch() -> None:
|
|
141
|
+
connection.wait([proc.sentinel])
|
|
142
|
+
if proc.exitcode not in (0, None):
|
|
143
|
+
failed_children.add(sec_id)
|
|
144
|
+
for ev in events:
|
|
145
|
+
ev.set()
|
|
146
|
+
|
|
147
|
+
threading.Thread(target=_watch, daemon=True,
|
|
148
|
+
name=f"sec-watch-{sec_id}").start()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _wait_with_liveness(
|
|
152
|
+
event: 'EventType',
|
|
153
|
+
sec_id: str,
|
|
154
|
+
sec_processes: 'dict[str, BaseProcess] | None',
|
|
155
|
+
failed_children: 'set[str] | None' = None,
|
|
156
|
+
) -> None:
|
|
157
|
+
"""
|
|
158
|
+
Wait for ``event`` without deadlocking on a dead security process.
|
|
159
|
+
|
|
160
|
+
With a ``failed_children`` registry (see :func:`watch_security_child`)
|
|
161
|
+
the wait is a plain unbounded ``event.wait()`` — the cheap, non-polling
|
|
162
|
+
path (macOS emulates TIMED multiprocessing waits with a select() polling
|
|
163
|
+
loop whose wake latency is disastrous per bar) — and a wake caused by the
|
|
164
|
+
death watcher raises ``RuntimeError``. Without a registry, fall back to
|
|
165
|
+
polling ``proc.is_alive()`` on a timed wait.
|
|
166
|
+
|
|
167
|
+
Same-context and ignored sec_ids have no associated Process — they use
|
|
168
|
+
the plain unbounded ``event.wait()`` because their signalling is driven
|
|
169
|
+
by the chart itself, not a separate process.
|
|
170
|
+
"""
|
|
171
|
+
if sec_processes is None or sec_id not in sec_processes:
|
|
172
|
+
event.wait()
|
|
173
|
+
return
|
|
174
|
+
if failed_children is not None:
|
|
175
|
+
event.wait()
|
|
176
|
+
if sec_id in failed_children:
|
|
177
|
+
proc = sec_processes[sec_id]
|
|
178
|
+
raise RuntimeError(
|
|
179
|
+
f"Security process for '{sec_id}' died unexpectedly "
|
|
180
|
+
f"(exit code: {proc.exitcode})"
|
|
181
|
+
)
|
|
182
|
+
return
|
|
183
|
+
proc = sec_processes[sec_id]
|
|
184
|
+
while not event.wait(timeout=_LIVENESS_POLL_SECONDS):
|
|
185
|
+
if not proc.is_alive():
|
|
186
|
+
raise RuntimeError(
|
|
187
|
+
f"Security process for '{sec_id}' died unexpectedly "
|
|
188
|
+
f"(exit code: {proc.exitcode})"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@dataclass
|
|
193
|
+
class SecurityState:
|
|
194
|
+
"""Per-security-context runtime state."""
|
|
195
|
+
sec_id: str
|
|
196
|
+
timeframe: str
|
|
197
|
+
gaps_on: bool
|
|
198
|
+
same_timeframe: bool
|
|
199
|
+
resampler: Resampler | None # None only if same_timeframe AND same_symbol
|
|
200
|
+
tz: ZoneInfo
|
|
201
|
+
|
|
202
|
+
# Multiprocessing events (shared between chart and security processes)
|
|
203
|
+
data_ready: EventType = field(default_factory=Event)
|
|
204
|
+
advance_event: EventType = field(default_factory=Event)
|
|
205
|
+
done_event: EventType = field(default_factory=Event)
|
|
206
|
+
stop_event: EventType = field(default_factory=Event)
|
|
207
|
+
|
|
208
|
+
# Cross-process mutex protecting this slot's ResultBlock + sync metadata.
|
|
209
|
+
# Held by writers (write_result/write_na) and by cross-context readers in
|
|
210
|
+
# security children. Chart-side reads also acquire it for uniformity, but
|
|
211
|
+
# never contend (data_ready already gates them).
|
|
212
|
+
result_lock: LockType = field(default_factory=Lock)
|
|
213
|
+
|
|
214
|
+
# LTF mode (lower timeframe → array return)
|
|
215
|
+
is_ltf: bool = False
|
|
216
|
+
|
|
217
|
+
# Plain ``request.security()`` with a timeframe FINER than the chart's
|
|
218
|
+
# (scalar return, unlike ``is_ltf``). TradingView merge rule (verified on
|
|
219
|
+
# captured references): ``lookahead_off`` returns the expression's value on
|
|
220
|
+
# the LAST intrabar of each chart bar, ``lookahead_on`` on the FIRST. The
|
|
221
|
+
# chart side therefore targets the chart bar's own period end (OFF) or open
|
|
222
|
+
# (ON) and the child's last per-intrabar write wins. ``plain_ltf_span_ms``
|
|
223
|
+
# caches the security period in ms for the live developing-bar clamp.
|
|
224
|
+
plain_ltf: bool = False
|
|
225
|
+
plain_ltf_span_ms: int = 0
|
|
226
|
+
|
|
227
|
+
# Synthetic chart type requested via ``ticker.heikinashi()`` etc. ``None`` is
|
|
228
|
+
# an ordinary feed. When set (currently only ``"heikinashi"``), it is passed
|
|
229
|
+
# to the security child, which applies the chart-type transform per bar
|
|
230
|
+
# (backtest and live alike) and flips the matching ``chart.*`` builtin. LTF
|
|
231
|
+
# (sub-bar) chart types are rejected at spawn.
|
|
232
|
+
chart_type: str | None = None
|
|
233
|
+
|
|
234
|
+
# LTF prefix-skip (chart-side, backtest/file-backed only). The LTF child's
|
|
235
|
+
# ``.ohlcv`` feed first bar open, in ms. The child includes intrabars with
|
|
236
|
+
# ``bar_open <= target_time`` and the historical target is the chart bar's
|
|
237
|
+
# last ms (``chart_off``), so a chart bar contains an intrabar only when its
|
|
238
|
+
# period end reaches the feed (``target_time >= ltf_first_ms``). A chart bar
|
|
239
|
+
# whose whole period ends before the feed's first open therefore yields an
|
|
240
|
+
# empty array unconditionally (TradingView returns ``na`` before the LTF
|
|
241
|
+
# series begins). ``__sec_signal__`` then skips the per-bar signal+wait
|
|
242
|
+
# handshake for that idle prefix and ``__sec_read__`` returns the empty-array
|
|
243
|
+
# default without touching shared memory. ``None`` disables the optimization
|
|
244
|
+
# (live ``PluginSymbol`` streams have no static first bar), restoring the
|
|
245
|
+
# original per-chart-bar signal. Populated by ``load_ltf_first_ms`` at child
|
|
246
|
+
# spawn.
|
|
247
|
+
ltf_first_ms: int | None = None
|
|
248
|
+
|
|
249
|
+
# Lookahead mode (Pine `lookahead=barmerge.lookahead_*`). Drives whether
|
|
250
|
+
# the security process should emit a ghost-bar write step on chart bars
|
|
251
|
+
# that fall inside an unclosed HTF period.
|
|
252
|
+
lookahead: Lookahead = Lookahead.OFF
|
|
253
|
+
|
|
254
|
+
# Per-sec_id HTF aggregator (chart-side). Populated by
|
|
255
|
+
# ``setup_security_states`` for every same-symbol HTF context — drives
|
|
256
|
+
# the live-mode closed-bar transport (all lookahead modes) and, for
|
|
257
|
+
# ``Lookahead.ON``, the developing-bar transport. None for same-TF, LTF,
|
|
258
|
+
# and cross-symbol HTF (chart-derived OHLCV would be the wrong instrument).
|
|
259
|
+
htf_aggregator: HTFAggregator | None = None
|
|
260
|
+
|
|
261
|
+
# Cross-symbol HTF + ``Lookahead.ON``: the containing developing bar
|
|
262
|
+
# cannot be aggregated (chart OHLCV is the wrong instrument), so the
|
|
263
|
+
# chart-side read returns ``na`` on every chart bar inside an open HTF
|
|
264
|
+
# period. The subprocess still advances on HTF period closes, so
|
|
265
|
+
# ``close[1]`` on the first chart bar of a fresh HTF period returns the
|
|
266
|
+
# just-closed cross-symbol HTF close — the TV ``lookahead_on + close[1]``
|
|
267
|
+
# idiom continues to work. Applies in both historical and live mode;
|
|
268
|
+
# backtest never silently emits a value live could not produce.
|
|
269
|
+
na_on_developing: bool = False
|
|
270
|
+
|
|
271
|
+
# True once the ScriptRunner enters live mode (``barstate.ishistory=False``).
|
|
272
|
+
# Chart-side ``__sec_signal__`` consults this to gate the developing-bar
|
|
273
|
+
# transport — historical bars never emit developing OHLCV.
|
|
274
|
+
is_live: bool = False
|
|
275
|
+
|
|
276
|
+
# True when this is an LTF (``request.security_lower_tf``) context backed by a
|
|
277
|
+
# live streaming source (a :class:`PluginSymbol`, no static ``.ohlcv`` file).
|
|
278
|
+
# Such a context has no ``ltf_first_ms`` (the loader is skipped) and its
|
|
279
|
+
# subprocess pulls intrabars from its own streamer, so ``__sec_signal__``
|
|
280
|
+
# routes every round — warmup replay included — through the LTF-window path
|
|
281
|
+
# (``FLAG_LTF_WINDOW``) rather than the file-backed read-ahead path. Set at
|
|
282
|
+
# setup; distinct from ``is_live`` (a lifecycle phase that flips only after
|
|
283
|
+
# warmup) and from ``ltf_first_ms is None`` (which also matches an empty
|
|
284
|
+
# static feed that has no streamer).
|
|
285
|
+
ltf_live_stream: bool = False
|
|
286
|
+
|
|
287
|
+
# Intraday session anchoring (chart-side). Populated by
|
|
288
|
+
# ``setup_security_states`` only when this security's session opens off the
|
|
289
|
+
# requested HTF grid (e.g. equities 09:30 at 1H). ``None`` selects the pure
|
|
290
|
+
# UTC clock-floor fast path in ``Resampler.get_bar_time`` — zero overhead for
|
|
291
|
+
# 24/7, on-hour, and session-aligned instruments. ``session_tz`` is the
|
|
292
|
+
# security's own exchange timezone (correct even for cross-symbol HTF).
|
|
293
|
+
session_starts: 'list[SymInfoSession] | None' = None
|
|
294
|
+
session_tz: ZoneInfo | None = None
|
|
295
|
+
|
|
296
|
+
# Daily/weekly/monthly HTF confirmation (chart-side). The child's data file
|
|
297
|
+
# realizes the actual trading calendar, so confirmation rides the child's
|
|
298
|
+
# real bar opens instead of an arithmetic grid that assumes a bar on every
|
|
299
|
+
# calendar period — essential for sparse daily series (ECONOMICS macro data,
|
|
300
|
+
# dividends) where the grid would confirm phantom periods and the subprocess
|
|
301
|
+
# would ``write_na`` into empty windows, wiping the ``gaps_off`` forward-fill.
|
|
302
|
+
# Two strategies, by ``bar_opens_multiperiod`` (see ``_get_confirmed_time``):
|
|
303
|
+
# * multi-period (nD/nW/nM): WALK the opens — the grid cannot reproduce
|
|
304
|
+
# TradingView's scheduled multi-period boundaries (holiday calendar).
|
|
305
|
+
# * single-period (1D/1W/1M): the grid gives the correct calendar close
|
|
306
|
+
# instant; CLAMP it to the latest real open so a sparse child still
|
|
307
|
+
# forward-fills its last value between bars instead of confirming late.
|
|
308
|
+
# Populated by ``load_htf_bar_opens`` at child spawn (backtest only).
|
|
309
|
+
# ``chart_off`` is the chart bar span minus one ms for
|
|
310
|
+
# intraday/seconds charts (0 for D/W/M charts); ``_get_confirmed_time``
|
|
311
|
+
# derives the chart bar's close instant from it for HTF confirmation.
|
|
312
|
+
# ``sec_grid_args`` are the security's own (tz, session_starts,
|
|
313
|
+
# opening_hours, mode) for the past-end-of-data fallback grid.
|
|
314
|
+
bar_opens: list[int] | None = None
|
|
315
|
+
# Session-bounded intraday HTF only: the scheduled session-end instant (ms)
|
|
316
|
+
# of each ``bar_opens`` entry, derived from ``opening_hours``.
|
|
317
|
+
# ``_get_confirmed_time`` then confirms such a bar on its session end
|
|
318
|
+
# (calendar-known) instead of the arithmetic next-period boundary, which a
|
|
319
|
+
# non-trading gap before the next session would push a full period late.
|
|
320
|
+
# ``None`` for D/W/M, sessionless and dense feeds (they keep the grid clamp).
|
|
321
|
+
bar_closes: list[int] | None = None
|
|
322
|
+
bar_opens_multiperiod: bool = False
|
|
323
|
+
bar_ptr: int = -1
|
|
324
|
+
chart_off: int = 0
|
|
325
|
+
sec_grid_args: tuple | None = None
|
|
326
|
+
|
|
327
|
+
# LTF window on a daily/weekly/monthly CHART (chart-side, file-backed). A
|
|
328
|
+
# single-period civil D/W/M chart bar has no fixed arithmetic span
|
|
329
|
+
# (``chart_off == 0``), so the Phase 1 ``chart_time + chart_off`` target
|
|
330
|
+
# degrades to the bar open and the child would re-collect the *previous*
|
|
331
|
+
# period. When the chart is a single-period civil D/W/M timeframe,
|
|
332
|
+
# ``chart_resampler`` (with ``chart_dwm_modifier`` 'D'/'W'/'M') is set for
|
|
333
|
+
# every single-period D/W/M chart so ``__sec_signal__`` can target the chart
|
|
334
|
+
# bar's civil period end (``_next_civil_period_open`` minus one ms) — the
|
|
335
|
+
# bar's OWN period ``[T, next_civil_open)``. Both stay ``None``/``''`` for
|
|
336
|
+
# intraday charts (the ``chart_off`` fast path) and for multi-period D/W/M
|
|
337
|
+
# charts (excluded at setup). Session-anchored D/W/M charts DO get these set,
|
|
338
|
+
# but the per-bar civil-anchored guard in ``__sec_signal__``
|
|
339
|
+
# (``get_bar_time(chart_time) == chart_time``) falls back to ``chart_off``
|
|
340
|
+
# for a bar that does not open on the civil boundary — a correct window there
|
|
341
|
+
# needs the chart's real bar opens, not a civil-calendar guess, and no
|
|
342
|
+
# TradingView ground truth exists for it (documented limitation).
|
|
343
|
+
chart_resampler: Resampler | None = None
|
|
344
|
+
chart_dwm_modifier: str = ''
|
|
345
|
+
|
|
346
|
+
# Tracking (chart-side only)
|
|
347
|
+
last_confirmed: int = 0
|
|
348
|
+
needs_wait: bool = False
|
|
349
|
+
new_period: bool = False
|
|
350
|
+
|
|
351
|
+
# Set by ``__sec_signal__`` when an LTF chart bar precedes the feed (see
|
|
352
|
+
# ``ltf_first_ms``): no handshake ran this bar, so ``__sec_read__`` returns
|
|
353
|
+
# the empty-array default directly instead of waiting on shared memory.
|
|
354
|
+
ltf_skip: bool = False
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _get_confirmed_time(state: SecurityState, chart_time: int) -> int:
|
|
358
|
+
"""
|
|
359
|
+
Determine which security period the subprocess should advance to.
|
|
360
|
+
|
|
361
|
+
Same timeframe: a chart bar and its same-time security bar close at the same
|
|
362
|
+
instant, so the target is the chart bar's own open — clamped to the latest
|
|
363
|
+
real child bar open when ``bar_opens`` is loaded (gappy cross-symbol feed,
|
|
364
|
+
e.g. a session-bounded bond yield on a 24/7 crypto chart). Between real bars
|
|
365
|
+
nothing new is confirmed, so ``gaps_off`` forward-fills the last real value
|
|
366
|
+
and ``gaps_on`` emits ``na`` — TradingView never evaluates the expression on
|
|
367
|
+
the writer's synthetic gap-fill bars.
|
|
368
|
+
|
|
369
|
+
Daily/weekly/monthly HTF (when ``bar_opens`` is loaded — see
|
|
370
|
+
``SecurityState.bar_opens``): confirmation rides the child's real bar opens
|
|
371
|
+
so a sparse child (scattered macro days) forward-fills its last value
|
|
372
|
+
instead of confirming phantom calendar periods. Multi-period walks the opens
|
|
373
|
+
directly; single-period clamps the arithmetic grid's calendar-close target
|
|
374
|
+
to the latest real open. Falls back to the bare arithmetic grid when
|
|
375
|
+
``bar_opens`` is ``None`` (intraday HTF, live streams, unit tests).
|
|
376
|
+
|
|
377
|
+
HTF:
|
|
378
|
+
* ``OFF`` / ``LAST_CLOSED``: target is the most recent HTF period that has
|
|
379
|
+
CLOSED by the current chart bar's close instant. TradingView's historical
|
|
380
|
+
``lookahead_off`` merge rule: an HTF bar's final value is carried already
|
|
381
|
+
by the chart bar whose close coincides with the HTF bar's close (the
|
|
382
|
+
period's last chart bar), not by the next period's first bar.
|
|
383
|
+
* ``ON`` (same-symbol HTF, an aggregator exists): target is the CONTAINING
|
|
384
|
+
period's opening time — the subprocess steps into the developing HTF bar.
|
|
385
|
+
Live mode supplies developing OHLCV via the SyncBlock; historical/backtest
|
|
386
|
+
mode reads the containing period's already-complete bar straight from the
|
|
387
|
+
child's data file (TV's lookahead_on future-leak for a bare ``close``, the
|
|
388
|
+
just-closed prior period for an inner ``close[1]``). Cross-symbol HTF has
|
|
389
|
+
no aggregator and keeps OFF (last-closed) semantics.
|
|
390
|
+
|
|
391
|
+
:param state: Security context state
|
|
392
|
+
:param chart_time: Current chart bar time in milliseconds
|
|
393
|
+
:return: Target time in milliseconds
|
|
394
|
+
"""
|
|
395
|
+
if state.same_timeframe:
|
|
396
|
+
if state.bar_opens is None:
|
|
397
|
+
return chart_time
|
|
398
|
+
# Gappy same-TF cross-symbol feed: clamp to the latest real child bar
|
|
399
|
+
# open. ``chart_time`` and ``bar_opens`` are both monotonic across chart
|
|
400
|
+
# bars, so the persistent ``bar_ptr`` only ever advances. ``ptr == -1``
|
|
401
|
+
# means the chart still precedes the first real security bar — return
|
|
402
|
+
# ``last_confirmed`` (0) so nothing is confirmed yet and the read stays
|
|
403
|
+
# ``na``, matching TradingView before the security series begins.
|
|
404
|
+
opens = state.bar_opens
|
|
405
|
+
n = len(opens)
|
|
406
|
+
ptr = state.bar_ptr
|
|
407
|
+
while ptr + 1 < n and opens[ptr + 1] <= chart_time:
|
|
408
|
+
ptr += 1
|
|
409
|
+
state.bar_ptr = ptr
|
|
410
|
+
if ptr >= 0:
|
|
411
|
+
return opens[ptr]
|
|
412
|
+
return state.last_confirmed
|
|
413
|
+
|
|
414
|
+
resampler = state.resampler
|
|
415
|
+
assert resampler is not None
|
|
416
|
+
|
|
417
|
+
# The chart bar's close instant. ``chart_off`` is span-1 for intraday and
|
|
418
|
+
# seconds charts; D/W/M chart bars have no fixed arithmetic span
|
|
419
|
+
# (``chart_off == 0``), so confirmation degrades to the bar's open instant.
|
|
420
|
+
close_time = chart_time + state.chart_off + 1
|
|
421
|
+
|
|
422
|
+
if state.bar_opens is not None and state.bar_opens_multiperiod:
|
|
423
|
+
# Multi-period walk: advance the pointer to the child bar the chart bar's
|
|
424
|
+
# close instant falls in; entering bar ``ptr`` closes every bar before it
|
|
425
|
+
opens = state.bar_opens
|
|
426
|
+
ptr = state.bar_ptr
|
|
427
|
+
n = len(opens)
|
|
428
|
+
advanced = False
|
|
429
|
+
while ptr + 1 < n and opens[ptr + 1] <= close_time:
|
|
430
|
+
ptr += 1
|
|
431
|
+
advanced = True
|
|
432
|
+
state.bar_ptr = ptr
|
|
433
|
+
if advanced and ptr >= 1:
|
|
434
|
+
return opens[ptr - 1]
|
|
435
|
+
if n and ptr == n - 1 and state.sec_grid_args is not None:
|
|
436
|
+
# The chart marched past the child's last bar: no child bar
|
|
437
|
+
# realizes the next period, so the arithmetic grid decides when
|
|
438
|
+
# the last bar is closed
|
|
439
|
+
sec_tz, ss, oh, mode = state.sec_grid_args
|
|
440
|
+
if resampler.get_bar_time(close_time, sec_tz, ss, oh, mode) > opens[-1]:
|
|
441
|
+
return opens[-1]
|
|
442
|
+
return state.last_confirmed
|
|
443
|
+
|
|
444
|
+
if (state.lookahead is Lookahead.ON
|
|
445
|
+
and state.htf_aggregator is not None):
|
|
446
|
+
# ``lookahead_on`` on a same-symbol HTF (an aggregator exists): step into
|
|
447
|
+
# the containing period rather than the last-closed one — TV faithful.
|
|
448
|
+
# In backtest/historical mode the containing period's bar is already
|
|
449
|
+
# complete in the child's data file, so the child reads its final OHLCV:
|
|
450
|
+
# a bare ``close`` reproduces TV's classical lookahead_on future-leak,
|
|
451
|
+
# while an ``<expr>[1]`` inside the security (the daily-pivot idiom
|
|
452
|
+
# ``security(sym, "D", close[1], lookahead_on)``) reads the just-closed
|
|
453
|
+
# prior period — the whole point of the idiom. ``lookahead_last_closed``
|
|
454
|
+
# remains the repaint-free alternative. Cross-symbol HTF has no
|
|
455
|
+
# aggregator (chart OHLCV is the wrong instrument), so it keeps
|
|
456
|
+
# closed-bar semantics and the chart-side read returns ``na`` while a
|
|
457
|
+
# period is open (``na_on_developing``). ``chart_time`` (not
|
|
458
|
+
# ``close_time``) selects the containing period so the last chart bar of
|
|
459
|
+
# a period still maps to that period, not the next.
|
|
460
|
+
if state.session_starts is not None:
|
|
461
|
+
# Off-grid intraday session → anchor HTF bars to the session open,
|
|
462
|
+
# using the security's own exchange timezone.
|
|
463
|
+
return resampler.get_bar_time(
|
|
464
|
+
chart_time, state.session_tz, state.session_starts)
|
|
465
|
+
return resampler.get_bar_time(chart_time, state.tz)
|
|
466
|
+
|
|
467
|
+
# OFF / LAST_CLOSED (and the historical ON fallback): the period preceding
|
|
468
|
+
# the one ``close_time`` falls in. When the chart bar's close lands exactly
|
|
469
|
+
# on a period boundary, ``get_bar_time`` floors it to that boundary and the
|
|
470
|
+
# period ending there is returned — the just-closed HTF bar is confirmed on
|
|
471
|
+
# its own last chart bar.
|
|
472
|
+
if state.session_starts is not None:
|
|
473
|
+
# Off-grid intraday session → anchor HTF bars to the session open,
|
|
474
|
+
# using the security's own exchange timezone.
|
|
475
|
+
period = resampler.get_bar_time(
|
|
476
|
+
close_time, state.session_tz, state.session_starts)
|
|
477
|
+
grid_target = resampler.get_bar_time(
|
|
478
|
+
period - 1, state.session_tz, state.session_starts)
|
|
479
|
+
else:
|
|
480
|
+
period = resampler.get_bar_time(close_time, state.tz)
|
|
481
|
+
grid_target = resampler.get_bar_time(period - 1, state.tz)
|
|
482
|
+
|
|
483
|
+
if state.bar_opens is None:
|
|
484
|
+
return grid_target
|
|
485
|
+
|
|
486
|
+
opens = state.bar_opens
|
|
487
|
+
n = len(opens)
|
|
488
|
+
ptr = state.bar_ptr
|
|
489
|
+
|
|
490
|
+
if state.bar_closes is not None:
|
|
491
|
+
# Session-bounded intraday HTF: each real bar closes at its session's
|
|
492
|
+
# scheduled end (calendar-known via ``opening_hours``), NOT at the
|
|
493
|
+
# arithmetic next-period boundary — a non-trading gap before the next
|
|
494
|
+
# session (e.g. the dead time between a futures day and night session)
|
|
495
|
+
# would otherwise delay confirmation by a full period. TradingView
|
|
496
|
+
# ``lookahead_off`` confirms the bar on its own last chart bar, whose
|
|
497
|
+
# close coincides with the session end. Confirm the latest real bar whose
|
|
498
|
+
# session end the chart bar's close (``close_time``) has reached.
|
|
499
|
+
# ``close_time`` and ``bar_closes`` are both monotonic across chart bars,
|
|
500
|
+
# so the persistent ``bar_ptr`` only ever advances.
|
|
501
|
+
closes = state.bar_closes
|
|
502
|
+
while ptr + 1 < n and closes[ptr + 1] <= close_time:
|
|
503
|
+
ptr += 1
|
|
504
|
+
state.bar_ptr = ptr
|
|
505
|
+
if ptr >= 0 and closes[ptr] <= close_time:
|
|
506
|
+
return opens[ptr]
|
|
507
|
+
return state.last_confirmed
|
|
508
|
+
|
|
509
|
+
# Single-period D/W/M with a loaded child: the grid above gives the correct
|
|
510
|
+
# calendar close instant, but the child may carry a bar only on scattered
|
|
511
|
+
# days (sparse macro series). Clamp the grid target to the latest real child
|
|
512
|
+
# open so the subprocess never advances into an empty window (which would
|
|
513
|
+
# ``write_na`` and wipe the forward-fill). Dense data has a bar on every
|
|
514
|
+
# period, so the clamp is a no-op and behaviour is identical to the bare
|
|
515
|
+
# grid; sparse data holds its last real value, matching TV ``gaps_off``.
|
|
516
|
+
# ``grid_target`` is monotonically non-decreasing across chart bars (it is
|
|
517
|
+
# ``get_bar_time(period - 1)`` of a monotonically increasing ``close_time``),
|
|
518
|
+
# so the persistent ``bar_ptr`` only ever advances. ``last_confirmed`` is a
|
|
519
|
+
# timestamp (0 = before any bar), returned while the chart precedes the first
|
|
520
|
+
# real open so nothing is confirmed yet.
|
|
521
|
+
while ptr + 1 < n and opens[ptr + 1] <= grid_target:
|
|
522
|
+
ptr += 1
|
|
523
|
+
state.bar_ptr = ptr
|
|
524
|
+
if ptr >= 0 and opens[ptr] <= grid_target:
|
|
525
|
+
return opens[ptr]
|
|
526
|
+
return state.last_confirmed
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _next_civil_period_open(modifier: str, current_ms: int, tz: ZoneInfo) -> int:
|
|
530
|
+
"""
|
|
531
|
+
Open time (ms) of the civil period immediately following the one that
|
|
532
|
+
contains ``current_ms``, for a single-period daily/weekly/monthly chart.
|
|
533
|
+
|
|
534
|
+
The next local calendar boundary (next day's midnight, next Monday, or the
|
|
535
|
+
first of next month) is constructed *directly* in ``tz`` and then converted
|
|
536
|
+
back to epoch ms — never by adding a fixed 24h / 7d / nominal-month delta —
|
|
537
|
+
so the result is correct across DST transitions and variable month lengths.
|
|
538
|
+
Used to window LTF intrabars into a D/W/M chart bar's own period
|
|
539
|
+
``[T, next_open)`` (the caller targets ``next_open - 1``).
|
|
540
|
+
|
|
541
|
+
:param modifier: Chart timeframe modifier, one of ``'D'``, ``'W'``, ``'M'``.
|
|
542
|
+
:param current_ms: The chart bar's open time in milliseconds.
|
|
543
|
+
:param tz: The chart's timezone (defines where the civil boundary falls).
|
|
544
|
+
:return: The next civil period's open time in milliseconds.
|
|
545
|
+
"""
|
|
546
|
+
cur = datetime.fromtimestamp(current_ms / 1000, tz)
|
|
547
|
+
if modifier == 'D':
|
|
548
|
+
nd = cur.date() + timedelta(days=1)
|
|
549
|
+
nxt = datetime(nd.year, nd.month, nd.day, tzinfo=tz)
|
|
550
|
+
elif modifier == 'W':
|
|
551
|
+
# Anchor to the bar's Monday, then step a full week — robust even if the
|
|
552
|
+
# bar open is not exactly the Monday boundary.
|
|
553
|
+
monday = cur.date() - timedelta(days=cur.weekday())
|
|
554
|
+
nd = monday + timedelta(days=7)
|
|
555
|
+
nxt = datetime(nd.year, nd.month, nd.day, tzinfo=tz)
|
|
556
|
+
else: # 'M'
|
|
557
|
+
year, month = (cur.year + 1, 1) if cur.month == 12 else (cur.year, cur.month + 1)
|
|
558
|
+
nxt = datetime(year, month, 1, tzinfo=tz)
|
|
559
|
+
return int(nxt.timestamp()) * 1000
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def create_chart_protocol(
|
|
563
|
+
states: dict[str, SecurityState],
|
|
564
|
+
sync_block: SyncBlock,
|
|
565
|
+
deferred_resolve_fn: 'Callable[[str, str, str | None], None] | None' = None,
|
|
566
|
+
lazy_spawn_fn: 'Callable[[str], None] | None' = None,
|
|
567
|
+
same_context_ids: 'set[str] | frozenset[str]' = frozenset(),
|
|
568
|
+
no_process_ids: 'set[str] | frozenset[str]' = frozenset(),
|
|
569
|
+
result_blocks: dict[str, ResultBlock] | None = None,
|
|
570
|
+
currency_conversions: dict[str, tuple[str, str]] | None = None,
|
|
571
|
+
sec_processes: 'dict[str, BaseProcess] | None' = None,
|
|
572
|
+
auto_rate_sec_ids: frozenset[str] = frozenset(),
|
|
573
|
+
failed_children: 'set[str] | None' = None,
|
|
574
|
+
) -> tuple:
|
|
575
|
+
"""
|
|
576
|
+
Create protocol functions for the **chart** process.
|
|
577
|
+
|
|
578
|
+
:param states: Per-security-context runtime states
|
|
579
|
+
:param sync_block: Shared memory sync block
|
|
580
|
+
:param deferred_resolve_fn: Optional callback for resolving deferred security contexts.
|
|
581
|
+
Called with (sec_id, symbol, timeframe) on first __sec_signal__.
|
|
582
|
+
:param lazy_spawn_fn: Optional callback for lazy-spawning static security processes.
|
|
583
|
+
Called with sec_id on first __sec_signal__ for static contexts.
|
|
584
|
+
:param same_context_ids: Security IDs that share the chart's symbol+timeframe.
|
|
585
|
+
These are handled directly by the chart (no separate process).
|
|
586
|
+
:param no_process_ids: Security IDs that have no process (same-context + ignored).
|
|
587
|
+
Signal/wait are skipped for these.
|
|
588
|
+
:param result_blocks: Result blocks for writing same-context values to shared memory.
|
|
589
|
+
:param currency_conversions: Maps sec_id → (from_currency, to_currency) for auto-conversion.
|
|
590
|
+
:param sec_processes: Live ``sec_id → Process`` map. Captured by reference, so
|
|
591
|
+
entries added by lazy/deferred spawn become visible to the
|
|
592
|
+
read/wait protocol functions. When provided, blocked waits
|
|
593
|
+
poll ``proc.is_alive()`` and raise instead of deadlocking
|
|
594
|
+
if a child dies.
|
|
595
|
+
:param auto_rate_sec_ids: Hidden ``__auto_rate_*`` sec_ids driving currency
|
|
596
|
+
rate sources. No Pine call signals them, so the
|
|
597
|
+
chart loop must call ``signal_rate_sources()``
|
|
598
|
+
once per bar to advance their subprocess and
|
|
599
|
+
refresh the ResultBlock the
|
|
600
|
+
:class:`CurrencyRateProvider` reads from.
|
|
601
|
+
:param failed_children: Shared registry filled by
|
|
602
|
+
:func:`watch_security_child` when a child dies
|
|
603
|
+
abnormally. Enables the cheap UNTIMED waits; when
|
|
604
|
+
None the waits fall back to liveness polling.
|
|
605
|
+
:return: (sec_signal, sec_write, sec_read, sec_wait, cleanup,
|
|
606
|
+
signal_rate_sources)
|
|
607
|
+
"""
|
|
608
|
+
readers: dict[str, ResultReader] = {
|
|
609
|
+
sid: ResultReader(sid) for sid in states
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
resolved: set[str] = set()
|
|
613
|
+
|
|
614
|
+
def __sec_signal__(sec_id: str, symbol: str | None = None,
|
|
615
|
+
timeframe: str | None = None, lookahead=None,
|
|
616
|
+
_scope_id=None):
|
|
617
|
+
from pynecore import lib
|
|
618
|
+
state = states[sec_id]
|
|
619
|
+
|
|
620
|
+
# Resolve deferred symbol/timeframe on first call. The two callbacks are
|
|
621
|
+
# NOT alternatives: in a script with both deferred and static contexts the
|
|
622
|
+
# deferred resolver no-ops for a static sec_id (and the runtime symbol
|
|
623
|
+
# argument is always present), so an elif here would leave every static
|
|
624
|
+
# context's subprocess unspawned and its first real read deadlocked.
|
|
625
|
+
# ``lazy_spawn_fn`` itself skips sids that already have a process.
|
|
626
|
+
if sec_id not in resolved:
|
|
627
|
+
resolved.add(sec_id)
|
|
628
|
+
if lookahead is not None:
|
|
629
|
+
# Input-derived (Pine "simple") lookahead: the transformer stored
|
|
630
|
+
# None in __security_contexts__ and passes the actual value here.
|
|
631
|
+
# Resolve the mode BEFORE the deferred symbol/timeframe callback,
|
|
632
|
+
# which recomputes ``na_on_developing`` from ``state.lookahead``.
|
|
633
|
+
state.lookahead = _lookahead_mode(lookahead)
|
|
634
|
+
# Static symbol/timeframe with deferred lookahead: mirror the
|
|
635
|
+
# setup-time cross-symbol decision (no aggregator ⇒ cross-symbol
|
|
636
|
+
# HTF). A deferred symbol/timeframe context is recomputed by the
|
|
637
|
+
# resolver below instead.
|
|
638
|
+
state.na_on_developing = (
|
|
639
|
+
not state.is_ltf and not state.plain_ltf
|
|
640
|
+
and not state.same_timeframe
|
|
641
|
+
and state.htf_aggregator is None
|
|
642
|
+
and state.lookahead is Lookahead.ON
|
|
643
|
+
)
|
|
644
|
+
if deferred_resolve_fn is not None and symbol is not None:
|
|
645
|
+
deferred_resolve_fn(sec_id, symbol, timeframe)
|
|
646
|
+
if lazy_spawn_fn is not None:
|
|
647
|
+
lazy_spawn_fn(sec_id)
|
|
648
|
+
|
|
649
|
+
# No-process contexts (same-context, ignored): skip advance/wait
|
|
650
|
+
if sec_id in no_process_ids:
|
|
651
|
+
if sec_id in same_context_ids:
|
|
652
|
+
state.new_period = True
|
|
653
|
+
state.data_ready.clear()
|
|
654
|
+
return
|
|
655
|
+
|
|
656
|
+
# noinspection PyProtectedMember
|
|
657
|
+
chart_time = lib._time
|
|
658
|
+
|
|
659
|
+
if state.plain_ltf:
|
|
660
|
+
# Plain (scalar) request.security with a timeframe FINER than the
|
|
661
|
+
# chart's. TradingView merge rule: ``lookahead_off`` returns the
|
|
662
|
+
# expression value on the LAST intrabar of the chart bar's own
|
|
663
|
+
# period, ``lookahead_on`` on the FIRST. The child's historical
|
|
664
|
+
# loop runs every feed bar with ``bar_open <= target`` and the
|
|
665
|
+
# last per-intrabar write wins, so the target IS the merge rule:
|
|
666
|
+
# the bar's period end for OFF, the bar's open for ON.
|
|
667
|
+
if state.lookahead is Lookahead.ON:
|
|
668
|
+
target_time = chart_time
|
|
669
|
+
elif (state.chart_dwm_modifier and state.chart_resampler is not None
|
|
670
|
+
and state.chart_resampler.get_bar_time(chart_time, state.tz)
|
|
671
|
+
== chart_time):
|
|
672
|
+
# Single-period civil D/W/M chart bar: no fixed arithmetic
|
|
673
|
+
# span (``chart_off`` is 0) — target the civil period end.
|
|
674
|
+
target_time = _next_civil_period_open(
|
|
675
|
+
state.chart_dwm_modifier, chart_time, state.tz) - 1
|
|
676
|
+
else:
|
|
677
|
+
target_time = chart_time + state.chart_off
|
|
678
|
+
# A developing live chart bar's period end lies in the future —
|
|
679
|
+
# clamp to the last surely-closed intrabar so the child never
|
|
680
|
+
# blocks waiting for intrabars that have not closed yet.
|
|
681
|
+
if state.is_live and not lib.barstate.isconfirmed and state.plain_ltf_span_ms:
|
|
682
|
+
now_ms = int(datetime.now().timestamp() * 1000)
|
|
683
|
+
elapsed_close = (now_ms // state.plain_ltf_span_ms
|
|
684
|
+
) * state.plain_ltf_span_ms - 1
|
|
685
|
+
if elapsed_close < target_time:
|
|
686
|
+
target_time = elapsed_close
|
|
687
|
+
# Prefix skip: a chart bar whose whole period ends before the LTF
|
|
688
|
+
# feed's first bar cannot contain an intrabar — TradingView
|
|
689
|
+
# returns ``na`` before the LTF series begins.
|
|
690
|
+
if state.ltf_first_ms is not None and target_time < state.ltf_first_ms:
|
|
691
|
+
state.ltf_skip = True
|
|
692
|
+
state.new_period = True
|
|
693
|
+
state.needs_wait = False
|
|
694
|
+
return
|
|
695
|
+
state.ltf_skip = False
|
|
696
|
+
state.new_period = True
|
|
697
|
+
state.data_ready.clear()
|
|
698
|
+
sync_block.set_target_time(sec_id, target_time)
|
|
699
|
+
state.advance_event.set()
|
|
700
|
+
state.needs_wait = True
|
|
701
|
+
return
|
|
702
|
+
|
|
703
|
+
if state.is_ltf:
|
|
704
|
+
# Live streaming LTF (PluginSymbol source): the chart bar may be
|
|
705
|
+
# developing, so read-ahead is impossible. The subprocess pulls
|
|
706
|
+
# intrabars from its own LTF streamer and builds the chart period's
|
|
707
|
+
# window (closed intrabars + the developing intrabar as the live last
|
|
708
|
+
# element). The parent ships only the period bounds and whether the
|
|
709
|
+
# chart bar is still developing; warmup replay (confirmed chart bars)
|
|
710
|
+
# flows through the same path and yields full closed periods.
|
|
711
|
+
if state.ltf_live_stream:
|
|
712
|
+
period_start = chart_time
|
|
713
|
+
if (state.chart_dwm_modifier and state.chart_resampler is not None
|
|
714
|
+
and state.chart_resampler.get_bar_time(chart_time, state.tz)
|
|
715
|
+
== chart_time):
|
|
716
|
+
period_end_exclusive = _next_civil_period_open(
|
|
717
|
+
state.chart_dwm_modifier, chart_time, state.tz)
|
|
718
|
+
else:
|
|
719
|
+
period_end_exclusive = chart_time + state.chart_off + 1
|
|
720
|
+
ltf_flags = sync_block.get_flags(sec_id) & ~(
|
|
721
|
+
FLAG_IS_DEVELOPING | FLAG_CLOSED_OVERRIDE
|
|
722
|
+
)
|
|
723
|
+
ltf_flags |= FLAG_LTF_WINDOW
|
|
724
|
+
if lib.barstate.isconfirmed:
|
|
725
|
+
ltf_flags &= ~FLAG_LTF_CHART_DEVELOPING
|
|
726
|
+
else:
|
|
727
|
+
ltf_flags |= FLAG_LTF_CHART_DEVELOPING
|
|
728
|
+
if state.is_live:
|
|
729
|
+
ltf_flags |= FLAG_LTF_LIVE_PHASE
|
|
730
|
+
else:
|
|
731
|
+
ltf_flags &= ~FLAG_LTF_LIVE_PHASE
|
|
732
|
+
sync_block.set_flags(sec_id, ltf_flags)
|
|
733
|
+
sync_block.set_target_time(sec_id, period_start)
|
|
734
|
+
sync_block.set_ltf_period_end(sec_id, period_end_exclusive)
|
|
735
|
+
state.ltf_skip = False
|
|
736
|
+
state.new_period = True
|
|
737
|
+
state.data_ready.clear()
|
|
738
|
+
state.advance_event.set()
|
|
739
|
+
state.needs_wait = True
|
|
740
|
+
return
|
|
741
|
+
|
|
742
|
+
# Historical/file-backed LTF: the child includes intrabars with
|
|
743
|
+
# ``bar_open <= target_time``. Target the chart bar's last ms so the
|
|
744
|
+
# child returns the bar's OWN period — matching TradingView. Adjacent
|
|
745
|
+
# bars tile with no gap or overlap (the prior bar targeted that ms
|
|
746
|
+
# minus one). An empty static feed (no streamer) has no intrabars to
|
|
747
|
+
# window, so keep the legacy chart-open target there.
|
|
748
|
+
if state.ltf_first_ms is None:
|
|
749
|
+
ltf_target_time = chart_time
|
|
750
|
+
elif (state.chart_dwm_modifier and state.chart_resampler is not None
|
|
751
|
+
and state.chart_resampler.get_bar_time(chart_time, state.tz)
|
|
752
|
+
== chart_time):
|
|
753
|
+
# Single-period civil D/W/M chart: ``chart_off`` is 0, so the
|
|
754
|
+
# period end is the next civil open minus one ms (not a fixed
|
|
755
|
+
# span). The civil-anchored guard above means we only do this
|
|
756
|
+
# when the chart bar actually opens on the civil boundary; an
|
|
757
|
+
# off-grid (session-anchored) D/W/M bar falls through to the
|
|
758
|
+
# ``chart_off`` path, keeping the documented limitation rather
|
|
759
|
+
# than mis-windowing on a civil-calendar guess.
|
|
760
|
+
ltf_target_time = _next_civil_period_open(
|
|
761
|
+
state.chart_dwm_modifier, chart_time, state.tz) - 1
|
|
762
|
+
else:
|
|
763
|
+
# Intraday/seconds chart: ``chart_off`` == span-1 gives the
|
|
764
|
+
# bar's own period ``[T, T+tf)``.
|
|
765
|
+
ltf_target_time = chart_time + state.chart_off
|
|
766
|
+
# Prefix skip: a chart bar whose whole period ends before the LTF
|
|
767
|
+
# feed's first bar (``target_time < ltf_first_ms``) cannot contain an
|
|
768
|
+
# intrabar, so the read is an empty array. Skip the cross-process
|
|
769
|
+
# signal+wait entirely — ``__sec_read__`` returns the empty-array
|
|
770
|
+
# default. Disabled (``ltf_first_ms is None``) for live streams,
|
|
771
|
+
# which keep signalling every bar.
|
|
772
|
+
if state.ltf_first_ms is not None and ltf_target_time < state.ltf_first_ms:
|
|
773
|
+
state.ltf_skip = True
|
|
774
|
+
state.new_period = True
|
|
775
|
+
state.needs_wait = False
|
|
776
|
+
return
|
|
777
|
+
state.ltf_skip = False
|
|
778
|
+
# LTF: every chart bar needs intrabar data — always signal. The
|
|
779
|
+
# period start bounds the flushed array to the bar's OWN intrabars:
|
|
780
|
+
# the child still replays any earlier feed bars for expression
|
|
781
|
+
# state, but their values are prefix, not array content.
|
|
782
|
+
state.new_period = True
|
|
783
|
+
state.data_ready.clear()
|
|
784
|
+
sync_block.set_ltf_period_start(sec_id, chart_time)
|
|
785
|
+
sync_block.set_target_time(sec_id, ltf_target_time)
|
|
786
|
+
state.advance_event.set()
|
|
787
|
+
state.needs_wait = True
|
|
788
|
+
return
|
|
789
|
+
|
|
790
|
+
# Live HTF transport — the chart aggregates its own OHLCV into the
|
|
791
|
+
# containing HTF bar via ``HTFAggregator`` and ships it to the
|
|
792
|
+
# subprocess on the SyncBlock. The static ``.ohlcv`` file cannot
|
|
793
|
+
# grow at runtime, so this transport is the *only* way for any
|
|
794
|
+
# lookahead mode to advance an HTF security context live.
|
|
795
|
+
#
|
|
796
|
+
# Phase 1 (closed-bar override): every HTF period close pushes the
|
|
797
|
+
# newly closed OHLCV to the subprocess synchronously. All lookahead
|
|
798
|
+
# modes use this phase — it is the live equivalent of reading the
|
|
799
|
+
# next ``.ohlcv`` bar in historical mode.
|
|
800
|
+
#
|
|
801
|
+
# Phase 2 (developing bar): only ``Lookahead.ON`` exposes the
|
|
802
|
+
# in-progress HTF bar with ``barstate.isconfirmed=False``; OFF and
|
|
803
|
+
# LAST_CLOSED stay repaint-free and skip this phase.
|
|
804
|
+
#
|
|
805
|
+
# Seed the aggregator on EVERY chart bar (warmup included). If we
|
|
806
|
+
# only fed it once ``is_live`` flipped, a live transition that
|
|
807
|
+
# happens mid-HTF-period would lose all warmup bars belonging to
|
|
808
|
+
# the in-progress period, and the first developing/closed override
|
|
809
|
+
# emitted live would carry partial OHLCV (open/high/low/volume
|
|
810
|
+
# missing the prior chart bars).
|
|
811
|
+
if state.htf_aggregator is not None:
|
|
812
|
+
chart_open = float(lib.open)
|
|
813
|
+
chart_high = float(lib.high)
|
|
814
|
+
chart_low = float(lib.low)
|
|
815
|
+
chart_close = float(lib.close)
|
|
816
|
+
raw_vol = lib.volume
|
|
817
|
+
chart_volume = 0.0 if raw_vol is None else float(raw_vol)
|
|
818
|
+
|
|
819
|
+
_, dev_bar, closed_bar = state.htf_aggregator.update(
|
|
820
|
+
chart_time, chart_open, chart_high, chart_low,
|
|
821
|
+
chart_close, chart_volume,
|
|
822
|
+
chart_confirmed=bool(lib.barstate.isconfirmed),
|
|
823
|
+
)
|
|
824
|
+
|
|
825
|
+
if state.is_live:
|
|
826
|
+
# Phase 1: synchronously deliver any just-closed HTF bar
|
|
827
|
+
if closed_bar is not None:
|
|
828
|
+
sync_block.set_developing_bar(
|
|
829
|
+
sec_id,
|
|
830
|
+
closed_bar.open, closed_bar.high, closed_bar.low,
|
|
831
|
+
closed_bar.close, closed_bar.volume,
|
|
832
|
+
closed_bar.period_start,
|
|
833
|
+
)
|
|
834
|
+
base_flags = (
|
|
835
|
+
sync_block.get_flags(sec_id) & ~FLAG_IS_DEVELOPING
|
|
836
|
+
) | FLAG_CLOSED_OVERRIDE
|
|
837
|
+
sync_block.set_flags(sec_id, base_flags)
|
|
838
|
+
sync_block.set_target_time(sec_id, closed_bar.period_start)
|
|
839
|
+
state.last_confirmed = closed_bar.period_start
|
|
840
|
+
state.data_ready.clear()
|
|
841
|
+
state.advance_event.set()
|
|
842
|
+
# Block until the subprocess finishes processing the closed
|
|
843
|
+
# bar (writes result, saves var_snapshot). For ON, this also
|
|
844
|
+
# ensures the developing-bar phase below cannot race ahead
|
|
845
|
+
# of the closed phase.
|
|
846
|
+
_wait_with_liveness(state.done_event, sec_id, sec_processes, failed_children)
|
|
847
|
+
state.done_event.clear()
|
|
848
|
+
|
|
849
|
+
# Phase 2: developing bar — only for ``Lookahead.ON``.
|
|
850
|
+
# ``dev_bar`` is None when the confirmed chart bar just
|
|
851
|
+
# completed the period (Phase 1 delivered it); no fresh
|
|
852
|
+
# developing bar exists until the next chart bar.
|
|
853
|
+
if state.lookahead is Lookahead.ON and dev_bar is not None:
|
|
854
|
+
sync_block.set_developing_bar(
|
|
855
|
+
sec_id,
|
|
856
|
+
dev_bar.open, dev_bar.high, dev_bar.low,
|
|
857
|
+
dev_bar.close, dev_bar.volume, dev_bar.period_start,
|
|
858
|
+
)
|
|
859
|
+
base_flags = (
|
|
860
|
+
sync_block.get_flags(sec_id) & ~FLAG_CLOSED_OVERRIDE
|
|
861
|
+
) | FLAG_IS_DEVELOPING
|
|
862
|
+
sync_block.set_flags(sec_id, base_flags)
|
|
863
|
+
sync_block.set_target_time(sec_id, dev_bar.period_start)
|
|
864
|
+
state.new_period = True
|
|
865
|
+
state.data_ready.clear()
|
|
866
|
+
state.advance_event.set()
|
|
867
|
+
state.needs_wait = True
|
|
868
|
+
return
|
|
869
|
+
|
|
870
|
+
# OFF / LAST_CLOSED in live mode: closed-bar transport only.
|
|
871
|
+
# ``new_period`` reflects whether a fresh HTF close just landed
|
|
872
|
+
# (drives ``gaps_on`` na/value selection in ``__sec_read__``).
|
|
873
|
+
# We already waited synchronously inside Phase 1, so no further
|
|
874
|
+
# wait is needed in ``__sec_wait__``.
|
|
875
|
+
state.new_period = closed_bar is not None
|
|
876
|
+
state.needs_wait = False
|
|
877
|
+
# Clear any stale developing flag from a prior ``Lookahead.ON``
|
|
878
|
+
# session (defensive — same SyncBlock slot).
|
|
879
|
+
if closed_bar is None:
|
|
880
|
+
stale_flags = sync_block.get_flags(sec_id) & ~(
|
|
881
|
+
FLAG_IS_DEVELOPING | FLAG_CLOSED_OVERRIDE
|
|
882
|
+
)
|
|
883
|
+
sync_block.set_flags(sec_id, stale_flags)
|
|
884
|
+
return
|
|
885
|
+
# Warmup with an HTF aggregator falls through to the closed-only
|
|
886
|
+
# flow below; the aggregator state has already advanced so the
|
|
887
|
+
# live transition starts with the correct in-progress HTF bar.
|
|
888
|
+
|
|
889
|
+
# Closed-only flow (historical / lookahead_off / lookahead_last_closed)
|
|
890
|
+
target_time = _get_confirmed_time(state, chart_time)
|
|
891
|
+
|
|
892
|
+
if target_time > state.last_confirmed:
|
|
893
|
+
state.last_confirmed = target_time
|
|
894
|
+
state.new_period = True
|
|
895
|
+
state.data_ready.clear()
|
|
896
|
+
# Make sure no stale developing/override flag leaks across modes.
|
|
897
|
+
stale_flags = sync_block.get_flags(sec_id) & ~(
|
|
898
|
+
FLAG_IS_DEVELOPING | FLAG_CLOSED_OVERRIDE
|
|
899
|
+
)
|
|
900
|
+
sync_block.set_flags(sec_id, stale_flags)
|
|
901
|
+
sync_block.set_target_time(sec_id, target_time)
|
|
902
|
+
state.advance_event.set()
|
|
903
|
+
state.needs_wait = True
|
|
904
|
+
else:
|
|
905
|
+
state.new_period = False
|
|
906
|
+
|
|
907
|
+
def __sec_write__(sec_id: str, value, _scope_id=None):
|
|
908
|
+
if sec_id in same_context_ids and result_blocks is not None:
|
|
909
|
+
with states[sec_id].result_lock:
|
|
910
|
+
write_result(result_blocks[sec_id], sync_block, value)
|
|
911
|
+
states[sec_id].data_ready.set()
|
|
912
|
+
|
|
913
|
+
def __sec_read__(sec_id: str, default=None, _scope_id=None):
|
|
914
|
+
# ``ignore_invalid_symbol=True`` may downgrade a live security to
|
|
915
|
+
# ``no-process`` after syminfo prefetch fails — no subprocess is
|
|
916
|
+
# ever spawned, so ``data_ready`` would never be set and a plain
|
|
917
|
+
# ``_wait_with_liveness`` would deadlock here. Short-circuit to
|
|
918
|
+
# ``default`` (Pine ``na``) so the script keeps running.
|
|
919
|
+
if sec_id in no_process_ids and sec_id not in same_context_ids:
|
|
920
|
+
return default
|
|
921
|
+
state = states[sec_id]
|
|
922
|
+
if state.ltf_skip:
|
|
923
|
+
# LTF chart bar before the feed began: ``__sec_signal__`` skipped the
|
|
924
|
+
# handshake, so the result is the empty-array default (identical to
|
|
925
|
+
# the empty-buffer flush an unskipped bar would have produced). The
|
|
926
|
+
# flag is current-bar-fresh: SecurityTransformer emits every
|
|
927
|
+
# context's ``__sec_signal__`` at ``main()``'s start, ahead of any
|
|
928
|
+
# ``__sec_read__``, so each read observes this bar's flag — the same
|
|
929
|
+
# signal-before-read invariant ``new_period``/``needs_wait`` rely on.
|
|
930
|
+
return default
|
|
931
|
+
_wait_with_liveness(state.data_ready, sec_id, sec_processes, failed_children)
|
|
932
|
+
|
|
933
|
+
if not state.is_ltf and not state.new_period:
|
|
934
|
+
# gaps_on emits ``na`` between HTF closes (Pine semantics).
|
|
935
|
+
# na_on_developing emits ``na`` while inside an open cross-symbol
|
|
936
|
+
# HTF period when lookahead_on is requested (developing bar cannot
|
|
937
|
+
# be aggregated). Both share the same shape: ``na`` whenever the
|
|
938
|
+
# chart bar is not opening a fresh HTF period.
|
|
939
|
+
if state.gaps_on or state.na_on_developing:
|
|
940
|
+
return default
|
|
941
|
+
|
|
942
|
+
with state.result_lock:
|
|
943
|
+
result = readers[sec_id].read(sync_block, default)
|
|
944
|
+
|
|
945
|
+
if currency_conversions and sec_id in currency_conversions and result is not default:
|
|
946
|
+
from ..lib import request
|
|
947
|
+
from math import isnan
|
|
948
|
+
from_cur, to_cur = currency_conversions[sec_id]
|
|
949
|
+
rate = request.currency_rate(from_cur, to_cur)
|
|
950
|
+
if not isnan(rate):
|
|
951
|
+
if isinstance(result, (int, float)):
|
|
952
|
+
result = result * rate
|
|
953
|
+
elif isinstance(result, tuple):
|
|
954
|
+
result = tuple(
|
|
955
|
+
v * rate if isinstance(v, (int, float)) else v for v in result
|
|
956
|
+
)
|
|
957
|
+
|
|
958
|
+
return result
|
|
959
|
+
|
|
960
|
+
def __sec_wait__(sec_id: str, _scope_id=None):
|
|
961
|
+
state = states[sec_id]
|
|
962
|
+
if state.needs_wait:
|
|
963
|
+
_wait_with_liveness(state.done_event, sec_id, sec_processes, failed_children)
|
|
964
|
+
state.done_event.clear()
|
|
965
|
+
state.needs_wait = False
|
|
966
|
+
|
|
967
|
+
def cleanup():
|
|
968
|
+
for r in readers.values():
|
|
969
|
+
r.close()
|
|
970
|
+
|
|
971
|
+
def signal_rate_sources():
|
|
972
|
+
"""Advance every auto-spawned rate-source subprocess by one bar.
|
|
973
|
+
|
|
974
|
+
No Pine call drives ``__auto_rate_*`` sec_ids (they are synthetic
|
|
975
|
+
contexts created by ``_autospawn_rate_sources``), so the chart loop
|
|
976
|
+
is the only place that can tick them forward. Each rate-source
|
|
977
|
+
subprocess runs the lightweight close-only loop in
|
|
978
|
+
``security_process._run_rate_source_loop``: advance → drain
|
|
979
|
+
newly-closed bars → write the latest close to its ResultBlock → set
|
|
980
|
+
data_ready. We wait synchronously for data_ready so the rate value
|
|
981
|
+
:meth:`CurrencyRateProvider._lookup_sec` reads later in the same
|
|
982
|
+
chart bar reflects the bars closed up to ``chart_time``.
|
|
983
|
+
"""
|
|
984
|
+
if not auto_rate_sec_ids:
|
|
985
|
+
return
|
|
986
|
+
from pynecore import lib
|
|
987
|
+
# noinspection PyProtectedMember
|
|
988
|
+
chart_time = lib._time
|
|
989
|
+
for sec_id in auto_rate_sec_ids:
|
|
990
|
+
if sec_id in no_process_ids or sec_id not in states:
|
|
991
|
+
continue
|
|
992
|
+
state = states[sec_id]
|
|
993
|
+
sync_block.set_target_time(sec_id, chart_time)
|
|
994
|
+
state.data_ready.clear()
|
|
995
|
+
state.advance_event.set()
|
|
996
|
+
_wait_with_liveness(state.data_ready, sec_id, sec_processes, failed_children)
|
|
997
|
+
|
|
998
|
+
return (
|
|
999
|
+
__sec_signal__, __sec_write__, __sec_read__, __sec_wait__,
|
|
1000
|
+
cleanup, signal_rate_sources,
|
|
1001
|
+
)
|
|
1002
|
+
|
|
1003
|
+
|
|
1004
|
+
def __ltf_unzip__(rows, n):
|
|
1005
|
+
"""Transpose a row-major LTF tuple buffer into Pine's column-major arrays.
|
|
1006
|
+
|
|
1007
|
+
``request.security_lower_tf(sym, tf, (e0, ..., e{n-1}))`` returns a tuple of
|
|
1008
|
+
``n`` arrays, where array ``i`` holds the per-intrabar values of ``e_i``. The
|
|
1009
|
+
LTF subprocess accumulates one ``(e0, ..., e{n-1})`` tuple per intrabar, so the
|
|
1010
|
+
raw result is row-major (a list of ``n``-tuples). This transposes it into the
|
|
1011
|
+
``n`` column arrays the tuple-unpack expects, returning ``n`` empty arrays when
|
|
1012
|
+
the chart bar has no intrabars (e.g. the lower-timeframe feed does not reach
|
|
1013
|
+
that period).
|
|
1014
|
+
|
|
1015
|
+
Inserted by ``SecurityTransformer`` only for tuple-valued
|
|
1016
|
+
``request.security_lower_tf()`` calls; scalar calls read the array directly.
|
|
1017
|
+
|
|
1018
|
+
:param rows: Per-intrabar value tuples (possibly empty when there are no
|
|
1019
|
+
intrabars).
|
|
1020
|
+
:param n: Tuple arity (number of expression elements).
|
|
1021
|
+
:return: Tuple of ``n`` lists, column-major.
|
|
1022
|
+
"""
|
|
1023
|
+
if not rows:
|
|
1024
|
+
return tuple([] for _ in range(n))
|
|
1025
|
+
return tuple(list(col) for col in zip(*rows))
|
|
1026
|
+
|
|
1027
|
+
|
|
1028
|
+
def create_security_protocol(
|
|
1029
|
+
sec_id: str,
|
|
1030
|
+
sync_block: SyncBlock,
|
|
1031
|
+
result_block: ResultBlock,
|
|
1032
|
+
all_sec_ids: list[str],
|
|
1033
|
+
result_locks: 'dict[str, LockType]',
|
|
1034
|
+
is_ltf: bool = False,
|
|
1035
|
+
) -> tuple:
|
|
1036
|
+
"""
|
|
1037
|
+
Create protocol functions for a **security** process.
|
|
1038
|
+
|
|
1039
|
+
In security context, __sec_signal__ and __sec_wait__ are no-ops (guarded by
|
|
1040
|
+
AST ``if __active_security__ is None`` checks). __sec_write__ writes to shared
|
|
1041
|
+
memory. __sec_read__ reads immediately without waiting (no deadlock).
|
|
1042
|
+
|
|
1043
|
+
When ``is_ltf=True``, __sec_write__ appends to an internal buffer instead of
|
|
1044
|
+
writing to shared memory. The caller must invoke ``flush()`` at the end of
|
|
1045
|
+
each round to write the accumulated array.
|
|
1046
|
+
|
|
1047
|
+
:param sec_id: This security context's ID (the only slot it writes to).
|
|
1048
|
+
:param sync_block: Shared memory sync block
|
|
1049
|
+
:param result_block: Shared memory result block for writing
|
|
1050
|
+
:param all_sec_ids: All security context IDs (for cross-context reads)
|
|
1051
|
+
:param result_locks: Per-slot ``multiprocessing.Lock`` keyed by sec_id.
|
|
1052
|
+
Writers acquire ``result_locks[sec_id]``; cross-context
|
|
1053
|
+
readers acquire ``result_locks[<peer sid>]``.
|
|
1054
|
+
:param is_ltf: If True, enable LTF accumulation mode.
|
|
1055
|
+
:return: (sec_signal, sec_write, sec_read, sec_wait, cleanup, flush,
|
|
1056
|
+
ltf_take_value, ltf_publish, buffer_len). ``flush``/
|
|
1057
|
+
``ltf_take_value``/``ltf_publish``/``buffer_len`` are None when
|
|
1058
|
+
``is_ltf=False``; ``flush``/``buffer_len`` serve the file-backed
|
|
1059
|
+
array path, ``ltf_take_value``/``ltf_publish`` the live LTF-window
|
|
1060
|
+
path.
|
|
1061
|
+
"""
|
|
1062
|
+
readers: dict[str, ResultReader] = {
|
|
1063
|
+
sid: ResultReader(sid) for sid in all_sec_ids
|
|
1064
|
+
}
|
|
1065
|
+
own_lock = result_locks[sec_id]
|
|
1066
|
+
|
|
1067
|
+
def __sec_signal__(_sid: str, _symbol=None, _timeframe=None, _lookahead=None,
|
|
1068
|
+
_scope_id=None):
|
|
1069
|
+
pass
|
|
1070
|
+
|
|
1071
|
+
if is_ltf:
|
|
1072
|
+
_buffer: list = []
|
|
1073
|
+
|
|
1074
|
+
def __sec_write__(_sid: str, value, _scope_id=None):
|
|
1075
|
+
_buffer.append(value)
|
|
1076
|
+
|
|
1077
|
+
def flush(skip: int = 0):
|
|
1078
|
+
"""Publish the round's intrabar array. ``skip`` drops the first N
|
|
1079
|
+
buffered values — feed bars the round replayed for expression
|
|
1080
|
+
state but which open BEFORE the chart bar's own period (a cold
|
|
1081
|
+
start mid-feed, or intrabars in a chart session gap). TradingView
|
|
1082
|
+
arrays carry only the bar's own period."""
|
|
1083
|
+
with own_lock:
|
|
1084
|
+
write_result(result_block, sync_block, _buffer[skip:])
|
|
1085
|
+
_buffer.clear()
|
|
1086
|
+
|
|
1087
|
+
def buffer_len() -> int:
|
|
1088
|
+
return len(_buffer)
|
|
1089
|
+
|
|
1090
|
+
def ltf_take_value():
|
|
1091
|
+
"""Return the value written by the latest intrabar run, clearing the
|
|
1092
|
+
buffer. Used by the live LTF-window path to capture one intrabar's
|
|
1093
|
+
expression value per ``__run_script_main`` instead of the whole
|
|
1094
|
+
accumulated array (which the path manages via its own window)."""
|
|
1095
|
+
if not _buffer:
|
|
1096
|
+
return None
|
|
1097
|
+
value = _buffer[-1]
|
|
1098
|
+
_buffer.clear()
|
|
1099
|
+
return value
|
|
1100
|
+
|
|
1101
|
+
def ltf_publish(values):
|
|
1102
|
+
"""Write a live LTF-window array under the result lock."""
|
|
1103
|
+
with own_lock:
|
|
1104
|
+
write_result(result_block, sync_block, list(values))
|
|
1105
|
+
else:
|
|
1106
|
+
def __sec_write__(_sid: str, value, _scope_id=None):
|
|
1107
|
+
with own_lock:
|
|
1108
|
+
write_result(result_block, sync_block, value)
|
|
1109
|
+
|
|
1110
|
+
flush = None
|
|
1111
|
+
ltf_take_value = None
|
|
1112
|
+
ltf_publish = None
|
|
1113
|
+
buffer_len = None
|
|
1114
|
+
|
|
1115
|
+
def __sec_read__(sid: str, default=None, _scope_id=None):
|
|
1116
|
+
# ``read_cached``: the child re-runs the script per (intra)bar, so a
|
|
1117
|
+
# peer context's unchanged result must not be re-unpickled every run —
|
|
1118
|
+
# a peer's deep-first-round LTF array would otherwise make the replay
|
|
1119
|
+
# quadratic (hours instead of seconds). See ResultReader.read_cached.
|
|
1120
|
+
with result_locks[sid]:
|
|
1121
|
+
return readers[sid].read_cached(sync_block, default)
|
|
1122
|
+
|
|
1123
|
+
def __sec_wait__(_sid: str, _scope_id=None):
|
|
1124
|
+
pass
|
|
1125
|
+
|
|
1126
|
+
def cleanup():
|
|
1127
|
+
for r in readers.values():
|
|
1128
|
+
r.close()
|
|
1129
|
+
|
|
1130
|
+
return (__sec_signal__, __sec_write__, __sec_read__, __sec_wait__, cleanup,
|
|
1131
|
+
flush, ltf_take_value, ltf_publish, buffer_len)
|
|
1132
|
+
|
|
1133
|
+
|
|
1134
|
+
# Representative dates for the off-grid session probe — one on each side of the
|
|
1135
|
+
# DST boundary so a session that lands on the tf grid only half the year is still
|
|
1136
|
+
# detected. Both are Mondays, so the weekday offset arithmetic below is exact.
|
|
1137
|
+
_WINTER_PROBE = date(2024, 1, 15)
|
|
1138
|
+
_SUMMER_PROBE = date(2024, 7, 15)
|
|
1139
|
+
|
|
1140
|
+
|
|
1141
|
+
def _needs_session_anchor(
|
|
1142
|
+
session_starts: 'list[SymInfoSession]',
|
|
1143
|
+
tzinfo: ZoneInfo | None,
|
|
1144
|
+
timeframe: str,
|
|
1145
|
+
) -> bool:
|
|
1146
|
+
"""
|
|
1147
|
+
Whether intraday ``timeframe`` bars need session anchoring for this market.
|
|
1148
|
+
|
|
1149
|
+
Session anchoring changes nothing when every declared session open already
|
|
1150
|
+
lands on the ``timeframe`` UTC-epoch grid (24/7, on-hour, session-aligned
|
|
1151
|
+
markets), so those keep the zero-overhead clock-floor fast path. The open is
|
|
1152
|
+
probed on both a winter and a summer date to cover both DST offsets. The test
|
|
1153
|
+
is deliberately conservative: it anchors whenever any open is off-grid.
|
|
1154
|
+
|
|
1155
|
+
:param session_starts: Per-trading-day primary opens.
|
|
1156
|
+
:param tzinfo: The market's exchange timezone.
|
|
1157
|
+
:param timeframe: The requested HTF string (e.g. ``"60"``).
|
|
1158
|
+
:return: True if the security loop must pass ``session_starts`` to anchor.
|
|
1159
|
+
"""
|
|
1160
|
+
if not session_starts:
|
|
1161
|
+
return False
|
|
1162
|
+
# Local import: ``core`` ↔ ``lib`` would otherwise form an import cycle
|
|
1163
|
+
# (mirrors the existing ``from pynecore.lib import ...`` uses in this file).
|
|
1164
|
+
from ..lib import timeframe as tf_module
|
|
1165
|
+
# noinspection PyProtectedMember
|
|
1166
|
+
modifier, _ = tf_module._process_tf(timeframe)
|
|
1167
|
+
if modifier not in ('S', ''):
|
|
1168
|
+
return False # D/W/M alignment is timezone-driven, not session-anchored
|
|
1169
|
+
tf_seconds = tf_module.in_seconds(timeframe)
|
|
1170
|
+
for probe in (_WINTER_PROBE, _SUMMER_PROBE):
|
|
1171
|
+
for s in session_starts:
|
|
1172
|
+
d = probe + timedelta(days=(s.day - probe.weekday()) % 7)
|
|
1173
|
+
open_sec = int(datetime(
|
|
1174
|
+
d.year, d.month, d.day,
|
|
1175
|
+
s.time.hour, s.time.minute, s.time.second,
|
|
1176
|
+
tzinfo=tzinfo,
|
|
1177
|
+
).timestamp())
|
|
1178
|
+
if open_sec % tf_seconds != 0:
|
|
1179
|
+
return True
|
|
1180
|
+
return False
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
def resolve_session_anchor(
|
|
1184
|
+
si: 'SymInfo | None',
|
|
1185
|
+
timeframe: str,
|
|
1186
|
+
fallback_tz: ZoneInfo,
|
|
1187
|
+
) -> 'tuple[list[SymInfoSession] | None, ZoneInfo | None]':
|
|
1188
|
+
"""
|
|
1189
|
+
Decide intraday HTF session anchoring for one security context.
|
|
1190
|
+
|
|
1191
|
+
Returns ``(session_starts, session_tz)`` to store on the ``SecurityState`` so
|
|
1192
|
+
``_get_confirmed_time`` anchors HTF bars to the session open, or ``(None,
|
|
1193
|
+
None)`` when the market opens on the ``timeframe`` grid (the clock-floor fast
|
|
1194
|
+
path). ``session_tz`` is the security's own exchange timezone — correct even
|
|
1195
|
+
for a cross-symbol HTF in a different session.
|
|
1196
|
+
|
|
1197
|
+
:param si: The security's own ``SymInfo`` (``None`` → no anchoring).
|
|
1198
|
+
:param timeframe: The resolved HTF string (e.g. ``"60"``).
|
|
1199
|
+
:param fallback_tz: Timezone used if the syminfo timezone is missing/invalid.
|
|
1200
|
+
"""
|
|
1201
|
+
if si is None or not getattr(si, 'session_starts', None):
|
|
1202
|
+
return None, None
|
|
1203
|
+
if si.timezone:
|
|
1204
|
+
try:
|
|
1205
|
+
# parse_timezone resolves both IANA names and UTC/GMT±HHMM offset
|
|
1206
|
+
# forms (e.g. "UTC-5"), which bare ZoneInfo() rejects.
|
|
1207
|
+
si_tz = parse_timezone(si.timezone)
|
|
1208
|
+
except (ValueError, KeyError):
|
|
1209
|
+
# Unknown / malformed timezone (ZoneInfoNotFoundError is a KeyError;
|
|
1210
|
+
# TimezoneNotFoundError is a ValueError).
|
|
1211
|
+
si_tz = fallback_tz
|
|
1212
|
+
else:
|
|
1213
|
+
si_tz = fallback_tz
|
|
1214
|
+
if _needs_session_anchor(si.session_starts, si_tz, timeframe):
|
|
1215
|
+
return si.session_starts, si_tz
|
|
1216
|
+
return None, None
|
|
1217
|
+
|
|
1218
|
+
|
|
1219
|
+
def _session_bar_closes(
|
|
1220
|
+
opens: list[int],
|
|
1221
|
+
tz: ZoneInfo | None,
|
|
1222
|
+
opening_hours: list[SymInfoInterval],
|
|
1223
|
+
period_ms: int,
|
|
1224
|
+
) -> list[int] | None:
|
|
1225
|
+
"""
|
|
1226
|
+
Close instant (epoch ms) of each intraday HTF bar: the earlier of its period
|
|
1227
|
+
end and its session's scheduled end.
|
|
1228
|
+
|
|
1229
|
+
An HTF bar covers ``[open, open + period)`` but never extends past its trading
|
|
1230
|
+
session, so it closes at ``min(open + period, session_end)``. When the period
|
|
1231
|
+
is at least as long as the session (one bar per session, e.g. a 720-minute bar
|
|
1232
|
+
on a 3-session palm-oil contract) the session end wins, and ``_get_confirmed_
|
|
1233
|
+
time`` confirms the bar there instead of at the arithmetic next-period boundary
|
|
1234
|
+
that the non-trading gap before the next session would push a full period late.
|
|
1235
|
+
When several bars fit inside a session (e.g. a 60-minute HTF) the period end
|
|
1236
|
+
wins and behaviour matches the plain grid. Each open's session end comes from
|
|
1237
|
+
the ``opening_hours`` interval that contains it (overnight intervals — ``end <=
|
|
1238
|
+
start`` — close on the following calendar day). A bar opening *after* midnight
|
|
1239
|
+
is matched to the PREVIOUS calendar day's overnight interval, whose session it
|
|
1240
|
+
belongs to (e.g. a ``21:00->02:00`` night session's ``01:00`` bar closes at the
|
|
1241
|
+
``02:00`` session end, not a full period later).
|
|
1242
|
+
|
|
1243
|
+
:param opens: HTF bar opens in epoch ms, ascending.
|
|
1244
|
+
:param tz: The security's exchange timezone.
|
|
1245
|
+
:param opening_hours: The security's ``SymInfo.opening_hours`` intervals.
|
|
1246
|
+
:param period_ms: The HTF period length in milliseconds.
|
|
1247
|
+
:return: A parallel list of close instants (epoch ms), or ``None`` if any open
|
|
1248
|
+
has no containing interval — the schedule does not fully describe the
|
|
1249
|
+
feed, so the caller keeps the arithmetic grid clamp rather than risk a
|
|
1250
|
+
wrong session end.
|
|
1251
|
+
"""
|
|
1252
|
+
from .resampler import crosses_midnight
|
|
1253
|
+
closes: list[int] = []
|
|
1254
|
+
for open_ms in opens:
|
|
1255
|
+
open_dt = datetime.fromtimestamp(open_ms / 1000, tz=tz)
|
|
1256
|
+
weekday = open_dt.weekday()
|
|
1257
|
+
prev_weekday = (weekday - 1) % 7
|
|
1258
|
+
open_time = open_dt.time()
|
|
1259
|
+
end_ms: int | None = None
|
|
1260
|
+
for interval in opening_hours:
|
|
1261
|
+
overnight = crosses_midnight(interval.start, interval.end)
|
|
1262
|
+
if (interval.day == weekday and interval.start <= open_time
|
|
1263
|
+
and (overnight or open_time < interval.end)):
|
|
1264
|
+
# Same-day session, or the pre-midnight leg of an overnight one
|
|
1265
|
+
# (which closes on the following calendar day).
|
|
1266
|
+
end_date = open_dt.date() + timedelta(days=1 if overnight else 0)
|
|
1267
|
+
elif overnight and interval.day == prev_weekday and open_time < interval.end:
|
|
1268
|
+
# After-midnight leg of the PREVIOUS day's overnight session: the
|
|
1269
|
+
# bar opens today but its session started yesterday and closes
|
|
1270
|
+
# today (e.g. a 21:00->02:00 night session's 01:00 bar).
|
|
1271
|
+
end_date = open_dt.date()
|
|
1272
|
+
else:
|
|
1273
|
+
continue
|
|
1274
|
+
candidate = int(
|
|
1275
|
+
datetime.combine(end_date, interval.end, tzinfo=tz).timestamp() * 1000)
|
|
1276
|
+
if end_ms is None or candidate < end_ms:
|
|
1277
|
+
end_ms = candidate
|
|
1278
|
+
if end_ms is None:
|
|
1279
|
+
return None
|
|
1280
|
+
# Whichever comes first: the bar's own period end, or the session end (a
|
|
1281
|
+
# non-trading gap before the next session must not delay confirmation).
|
|
1282
|
+
closes.append(min(open_ms + period_ms, end_ms))
|
|
1283
|
+
return closes
|
|
1284
|
+
|
|
1285
|
+
|
|
1286
|
+
def _dated_session_bar_closes(
|
|
1287
|
+
opens: list[int],
|
|
1288
|
+
tz: ZoneInfo | None,
|
|
1289
|
+
si: SymInfo,
|
|
1290
|
+
period_ms: int,
|
|
1291
|
+
overnight: dict[int, time],
|
|
1292
|
+
) -> list[int] | None:
|
|
1293
|
+
"""
|
|
1294
|
+
Close instants for an HTF feed whose exchange changed its session hours within
|
|
1295
|
+
the data range (effective-dated schedule history).
|
|
1296
|
+
|
|
1297
|
+
Like :func:`_session_bar_closes`, but each bar open is matched to the session
|
|
1298
|
+
schedule *variant* effective on its exchange-local trading day, so a backtest
|
|
1299
|
+
spanning a session-hours change confirms each side with its own schedule. The
|
|
1300
|
+
trading-day key (not the raw calendar date of the open) is what
|
|
1301
|
+
``request.security`` already uses to attribute overnight bars: a night bar
|
|
1302
|
+
opening 21:00 the evening before belongs to the next trading day and must take
|
|
1303
|
+
that day's variant -- keying on the raw open date would mis-assign the boundary
|
|
1304
|
+
bar by one day, exactly where a schedule change lives.
|
|
1305
|
+
|
|
1306
|
+
Consecutive opens resolving to the same variant index are grouped into one
|
|
1307
|
+
segment and handed to the UNCHANGED :func:`_session_bar_closes` with that
|
|
1308
|
+
variant's ``opening_hours``, so every segment runs the same, already-tested
|
|
1309
|
+
close-instant arithmetic. Grouping is by variant *index* (not object identity),
|
|
1310
|
+
so an ``A -> B -> A`` history yields three segments and the result is stable
|
|
1311
|
+
even if the resolver ever returns copies. Any segment the schedule cannot fully
|
|
1312
|
+
describe returns ``None``, propagated so the caller keeps the arithmetic grid
|
|
1313
|
+
clamp.
|
|
1314
|
+
|
|
1315
|
+
:param opens: HTF bar opens in epoch ms, ascending.
|
|
1316
|
+
:param tz: The security's exchange timezone.
|
|
1317
|
+
:param si: The security's :class:`SymInfo` (carries ``session_schedules``).
|
|
1318
|
+
:param period_ms: The HTF period length in milliseconds.
|
|
1319
|
+
:param overnight: Per-weekday rolling opens from ``overnight_opens``, used to
|
|
1320
|
+
roll each open to its trading day.
|
|
1321
|
+
:return: A parallel list of close instants, or ``None`` if any variant fails to
|
|
1322
|
+
describe its bars.
|
|
1323
|
+
"""
|
|
1324
|
+
from .resampler import trading_day
|
|
1325
|
+
# Resolve every open's variant index in one pass (trading-day keyed), then walk
|
|
1326
|
+
# maximal same-index runs. Setup-time only -- never on the per-bar hot path.
|
|
1327
|
+
idx = [si.schedule_index_for(trading_day(o // 1000, tz, overnight)) for o in opens]
|
|
1328
|
+
closes: list[int] = []
|
|
1329
|
+
i, n = 0, len(opens)
|
|
1330
|
+
while i < n:
|
|
1331
|
+
k = idx[i]
|
|
1332
|
+
j = i
|
|
1333
|
+
while j < n and idx[j] == k:
|
|
1334
|
+
j += 1
|
|
1335
|
+
oh = si.session_schedules[k].opening_hours
|
|
1336
|
+
seg = _session_bar_closes(opens[i:j], tz, oh, period_ms)
|
|
1337
|
+
if seg is None:
|
|
1338
|
+
return None
|
|
1339
|
+
closes.extend(seg)
|
|
1340
|
+
i = j
|
|
1341
|
+
return closes
|
|
1342
|
+
|
|
1343
|
+
|
|
1344
|
+
def load_htf_bar_opens(state: SecurityState, data_path: str) -> None:
|
|
1345
|
+
"""
|
|
1346
|
+
Load the child's real bar opens for HTF confirmation against the actual feed.
|
|
1347
|
+
|
|
1348
|
+
The arithmetic grid in ``_get_confirmed_time`` assumes a child bar exists at
|
|
1349
|
+
every grid period — true only for a DENSE feed. Two cases break it, and both
|
|
1350
|
+
confirm by riding the child's real bar opens instead:
|
|
1351
|
+
|
|
1352
|
+
* D/W/M — including the single-period ``D``/``W``/``M`` case: macro aggregates
|
|
1353
|
+
(ECONOMICS series, dividends) carry a bar only on scattered days, so the
|
|
1354
|
+
grid would emit a confirmation boundary for every calendar period and a
|
|
1355
|
+
chart bar landing on a day with no real child bar would advance the
|
|
1356
|
+
subprocess into an empty window — writing ``na`` and destroying the
|
|
1357
|
+
``gaps_off`` (TV default) forward-fill.
|
|
1358
|
+
* Gappy intraday HTF: ``OHLCVWriter`` forward-fills a session-gapped futures
|
|
1359
|
+
feed (e.g. a 720-minute HTF on a 3-session palm-oil contract) to a
|
|
1360
|
+
continuous grid, but the security child reads only the real bars
|
|
1361
|
+
(gap-compacted, see ``security_process``). The grid would then confirm
|
|
1362
|
+
phantom periods on the fills' timestamps. Dense intraday feeds keep the
|
|
1363
|
+
cheaper arithmetic grid (this stays a no-op for them); LTF contexts run
|
|
1364
|
+
their own intrabar machinery, not HTF confirmation.
|
|
1365
|
+
|
|
1366
|
+
A gappy SAME-TF cross-symbol feed (a session-bounded symbol requested at the
|
|
1367
|
+
chart's own TF on a 24/7 chart) rides the same intraday path: the child is
|
|
1368
|
+
gap-compacted, and ``_get_confirmed_time`` clamps the chart bar time to
|
|
1369
|
+
these opens so gap bars confirm nothing new.
|
|
1370
|
+
|
|
1371
|
+
Riding the real opens (clamp for single-period / intraday, walk for
|
|
1372
|
+
multi-period D/W/M) makes ``new_period`` fire only on real bars: between them
|
|
1373
|
+
``gaps_off`` holds the last value and ``gaps_on`` emits ``na``, both matching
|
|
1374
|
+
TradingView. The security's own grid parameters are loaded from its TOML for
|
|
1375
|
+
the past-end-of-data fallback.
|
|
1376
|
+
|
|
1377
|
+
:param state: Security context state (``state.timeframe`` already resolved)
|
|
1378
|
+
:param data_path: Path to the child's OHLCV data file
|
|
1379
|
+
"""
|
|
1380
|
+
# Local import: ``core`` ↔ ``lib`` would otherwise form an import cycle.
|
|
1381
|
+
from ..lib import timeframe as tf_module
|
|
1382
|
+
from .ohlcv_file import OHLCVReader
|
|
1383
|
+
from .resampler import grid_mode, overnight_opens, trading_day
|
|
1384
|
+
from .syminfo import SymInfo
|
|
1385
|
+
# noinspection PyProtectedMember
|
|
1386
|
+
modifier, multiplier = tf_module._process_tf(state.timeframe)
|
|
1387
|
+
is_dwm = modifier in ('D', 'W', 'M')
|
|
1388
|
+
|
|
1389
|
+
# LTF contexts (array windows and the scalar plain-LTF merge alike) never
|
|
1390
|
+
# use HTF confirmation — their target is the chart bar's own period.
|
|
1391
|
+
if state.is_ltf or state.plain_ltf:
|
|
1392
|
+
return
|
|
1393
|
+
|
|
1394
|
+
if not is_dwm:
|
|
1395
|
+
# Intraday HTF: only a GAPPY feed needs the real-opens clamp (see above);
|
|
1396
|
+
# a dense feed keeps the arithmetic grid.
|
|
1397
|
+
period_sec = tf_module.in_seconds(state.timeframe)
|
|
1398
|
+
with OHLCVReader(data_path) as reader:
|
|
1399
|
+
start_ts = reader.start_timestamp
|
|
1400
|
+
if start_ts is None:
|
|
1401
|
+
return
|
|
1402
|
+
opens = [candle.timestamp * 1000 for candle in reader.read_from(start_ts)]
|
|
1403
|
+
# A feed is dense only when its real bars tile the timeframe grid (file
|
|
1404
|
+
# interval == period). The row count alone is not enough: a session-
|
|
1405
|
+
# spaced feed whose bars sit wider than the period (e.g. a gap-free,
|
|
1406
|
+
# 24h-spaced 720-minute night future) has no gap fills, so
|
|
1407
|
+
# ``len(opens) == reader.size`` holds, yet its bars do NOT tile the
|
|
1408
|
+
# period grid and must still ride the session-close path below. A
|
|
1409
|
+
# single-record file has no interval to compare, so it keeps the dense
|
|
1410
|
+
# fast path as before.
|
|
1411
|
+
if len(opens) == reader.size and reader.interval in (None, period_sec):
|
|
1412
|
+
return # dense feed: the arithmetic grid is already correct
|
|
1413
|
+
# Gappy fixed-span intraday HTF: keep the arithmetic (fixed-span) grid for
|
|
1414
|
+
# the close instant, but CLAMP it to the latest real open so an empty
|
|
1415
|
+
# (gap) period holds the last real bar instead of advancing into a phantom
|
|
1416
|
+
# period and writing na — see ``_get_confirmed_time``.
|
|
1417
|
+
state.bar_opens_multiperiod = False
|
|
1418
|
+
else:
|
|
1419
|
+
# Multi-period (nD/nW/nM) walks the opens directly (the arithmetic grid
|
|
1420
|
+
# cannot reproduce TradingView's scheduled multi-period calendar). Single
|
|
1421
|
+
# period (1D/1W/1M) instead uses the grid for the calendar close instant
|
|
1422
|
+
# and only *clamps* to these opens — see ``_get_confirmed_time``.
|
|
1423
|
+
state.bar_opens_multiperiod = multiplier > 1
|
|
1424
|
+
with OHLCVReader(data_path) as reader:
|
|
1425
|
+
if reader.size == 1:
|
|
1426
|
+
# A single-record feed has no derivable interval, so
|
|
1427
|
+
# ``start_timestamp`` stays ``None`` and ``read_from`` bails —
|
|
1428
|
+
# ``opens`` would be empty and ``_get_confirmed_time`` would then
|
|
1429
|
+
# never confirm the lone bar (the child reads ``na`` forever).
|
|
1430
|
+
# This is reachable when a finer base feed spans exactly one
|
|
1431
|
+
# requested D/W/M period and resamples to a single aggregate.
|
|
1432
|
+
# Read the one bar directly so its open still anchors the clamp.
|
|
1433
|
+
opens = [reader.read(0).timestamp * 1000]
|
|
1434
|
+
else:
|
|
1435
|
+
start_ts = reader.start_timestamp
|
|
1436
|
+
opens = ([] if start_ts is None else
|
|
1437
|
+
[candle.timestamp * 1000 for candle in reader.read_from(start_ts)])
|
|
1438
|
+
|
|
1439
|
+
state.bar_opens = opens
|
|
1440
|
+
state.bar_ptr = -1
|
|
1441
|
+
|
|
1442
|
+
sec_tz: ZoneInfo | None = state.tz
|
|
1443
|
+
sec_starts = sec_hours = mode = None
|
|
1444
|
+
toml_path = Path(data_path).with_suffix('.toml')
|
|
1445
|
+
if toml_path.exists():
|
|
1446
|
+
si = SymInfo.load_toml(toml_path)
|
|
1447
|
+
try:
|
|
1448
|
+
sec_tz = parse_timezone(si.timezone) if si.timezone else state.tz
|
|
1449
|
+
except (ValueError, KeyError):
|
|
1450
|
+
sec_tz = state.tz
|
|
1451
|
+
sec_starts = si.session_starts or None
|
|
1452
|
+
sec_hours = si.opening_hours or None
|
|
1453
|
+
mode = grid_mode(si.type, si.opening_hours)
|
|
1454
|
+
|
|
1455
|
+
# Session-bounded intraday HTF (e.g. a futures contract's day/night
|
|
1456
|
+
# sessions): confirm each bar on its scheduled session end instead of the
|
|
1457
|
+
# arithmetic next-period boundary (see ``_get_confirmed_time``). Needs the
|
|
1458
|
+
# session schedule; ``None`` (no schedule, or a bar outside it) keeps the
|
|
1459
|
+
# grid clamp.
|
|
1460
|
+
if not is_dwm and sec_hours and not state.same_timeframe:
|
|
1461
|
+
period_ms = tf_module.in_seconds(state.timeframe) * 1000
|
|
1462
|
+
if si.has_schedule_history:
|
|
1463
|
+
# The trading-day roll keys off the flat (newest) session opens;
|
|
1464
|
+
# this Core path assumes the session OPEN / trading-day attribution
|
|
1465
|
+
# is stable across variants (close-only era changes, e.g. a futures
|
|
1466
|
+
# contract that shortened its night session). A symbol that shifts
|
|
1467
|
+
# its session START across eras needs the deferred session-anchoring
|
|
1468
|
+
# work -- the assumption is stated here in code, not only the docs.
|
|
1469
|
+
overnight = overnight_opens(sec_hours, sec_starts)
|
|
1470
|
+
# Surface that unsupported shape instead of silently mis-confirming:
|
|
1471
|
+
# an earlier variant whose overnight session OPENS at a different
|
|
1472
|
+
# time than the newest one (a session-START shift, not a close-only
|
|
1473
|
+
# change) is rolled to the wrong trading day by the newest-keyed
|
|
1474
|
+
# ``overnight`` above and can pick the wrong variant. Only a weekday
|
|
1475
|
+
# that is overnight in BOTH variants but at a different time counts;
|
|
1476
|
+
# a structurally different (e.g. day-only) era is handled by the
|
|
1477
|
+
# ``None`` fallback below, not a START shift.
|
|
1478
|
+
for variant in si.session_schedules[:-1]:
|
|
1479
|
+
vo = overnight_opens(variant.opening_hours, variant.session_starts)
|
|
1480
|
+
if any(overnight.get(d) is not None and t != overnight[d]
|
|
1481
|
+
for d, t in vo.items()):
|
|
1482
|
+
logger.warning(
|
|
1483
|
+
"%s:%s session schedule history changes the overnight "
|
|
1484
|
+
"session OPEN at variant effective %s; the dated HTF "
|
|
1485
|
+
"path attributes every bar by the newest variant's open, "
|
|
1486
|
+
"so bars near that change may confirm against the wrong "
|
|
1487
|
+
"variant. Session-START shifts are not yet supported "
|
|
1488
|
+
"(close-only era changes are).",
|
|
1489
|
+
si.prefix, si.ticker, variant.effective_from)
|
|
1490
|
+
break
|
|
1491
|
+
if opens:
|
|
1492
|
+
first_td = trading_day(opens[0] // 1000, sec_tz, overnight)
|
|
1493
|
+
earliest = si.session_schedules[0].effective_from
|
|
1494
|
+
if first_td < earliest:
|
|
1495
|
+
logger.warning(
|
|
1496
|
+
"%s:%s session schedule history starts %s but the HTF "
|
|
1497
|
+
"feed opens on trading day %s; the oldest variant was "
|
|
1498
|
+
"applied to the earlier bars. Add an earlier "
|
|
1499
|
+
"[[session_schedules]] variant for an exact backtest "
|
|
1500
|
+
"across that range.",
|
|
1501
|
+
si.prefix, si.ticker, earliest, first_td)
|
|
1502
|
+
state.bar_closes = _dated_session_bar_closes(
|
|
1503
|
+
opens, sec_tz, si, period_ms, overnight)
|
|
1504
|
+
else:
|
|
1505
|
+
state.bar_closes = _session_bar_closes(opens, sec_tz, sec_hours, period_ms)
|
|
1506
|
+
state.sec_grid_args = (sec_tz, sec_starts, sec_hours, mode)
|
|
1507
|
+
|
|
1508
|
+
|
|
1509
|
+
def load_ltf_first_ms(state: SecurityState, data_path: str) -> None:
|
|
1510
|
+
"""
|
|
1511
|
+
Record the LTF child feed's first bar open for the chart-side prefix skip.
|
|
1512
|
+
|
|
1513
|
+
``request.security_lower_tf()`` makes the chart block on a cross-process
|
|
1514
|
+
handshake for *every* chart bar, because any chart bar may contain
|
|
1515
|
+
intrabars. Chart bars whose whole period ends before the feed's very first
|
|
1516
|
+
bar never can: the child includes intrabars with ``bar_open <= target_time``
|
|
1517
|
+
and the historical target is the chart bar's last ms, so a chart bar whose
|
|
1518
|
+
period ends strictly below the feed's first open yields an empty intrabar
|
|
1519
|
+
array unconditionally — matching TradingView, which returns ``na`` before the
|
|
1520
|
+
lower-timeframe series begins. Recording that first open lets
|
|
1521
|
+
``__sec_signal__`` skip the signal+wait over the idle prefix and
|
|
1522
|
+
``__sec_read__`` return the empty-array default without touching shared
|
|
1523
|
+
memory. Backtest/file-backed only: a live ``PluginSymbol`` stream has no
|
|
1524
|
+
static first bar, so ``ltf_first_ms`` stays ``None`` and every chart bar
|
|
1525
|
+
signals as before. No-op for non-LTF contexts (``plain_ltf`` — the scalar
|
|
1526
|
+
lower-timeframe merge — uses the same prefix skip, so it loads too).
|
|
1527
|
+
|
|
1528
|
+
:param state: LTF security context state.
|
|
1529
|
+
:param data_path: Path to the child's OHLCV data file.
|
|
1530
|
+
"""
|
|
1531
|
+
if not state.is_ltf and not state.plain_ltf:
|
|
1532
|
+
return
|
|
1533
|
+
from .ohlcv_file import OHLCVReader
|
|
1534
|
+
with OHLCVReader(data_path) as reader:
|
|
1535
|
+
start_ts = reader.start_timestamp
|
|
1536
|
+
state.ltf_first_ms = None if start_ts is None else int(start_ts * 1000)
|
|
1537
|
+
|
|
1538
|
+
|
|
1539
|
+
def setup_security_states(
|
|
1540
|
+
contexts: dict[str, dict],
|
|
1541
|
+
chart_timeframe: str,
|
|
1542
|
+
tz: 'ZoneInfo',
|
|
1543
|
+
chart_symbol: str | None = None,
|
|
1544
|
+
chart_syminfo: 'SymInfo | None' = None,
|
|
1545
|
+
sec_syminfos: 'dict[str, SymInfo] | None' = None,
|
|
1546
|
+
) -> tuple[dict[str, SecurityState], SyncBlock, dict[str, ResultBlock]]:
|
|
1547
|
+
"""
|
|
1548
|
+
Initialize security states, shared memory, and events from ``__security_contexts__``.
|
|
1549
|
+
|
|
1550
|
+
:param contexts: The ``__security_contexts__`` dict from the script module.
|
|
1551
|
+
Keys are sec_ids, values are dicts with 'symbol', 'timeframe', 'gaps'.
|
|
1552
|
+
:param chart_timeframe: The chart's timeframe string (e.g., "5", "1D").
|
|
1553
|
+
:param tz: The chart's timezone.
|
|
1554
|
+
:param chart_symbol: The chart's ticker (e.g. ``"AAPL"``). Drives same-symbol
|
|
1555
|
+
gating for the live HTF transport — a cross-symbol HTF
|
|
1556
|
+
context gets no ``HTFAggregator`` because the chart-side
|
|
1557
|
+
OHLCV would be the wrong instrument. ``None`` (unit-test
|
|
1558
|
+
/ legacy callers without symbol context) is treated as
|
|
1559
|
+
"every HTF is same-symbol".
|
|
1560
|
+
:param chart_syminfo: The chart symbol's ``SymInfo``, used as the session
|
|
1561
|
+
source for same-symbol HTF anchoring. ``None`` disables
|
|
1562
|
+
anchoring unless a per-security syminfo is supplied.
|
|
1563
|
+
:param sec_syminfos: ``sec_id → SymInfo`` for cross-symbol contexts, used so
|
|
1564
|
+
each security anchors to its own session/timezone. ``None``
|
|
1565
|
+
falls back to ``chart_syminfo``.
|
|
1566
|
+
:return: (states, sync_block, result_blocks)
|
|
1567
|
+
"""
|
|
1568
|
+
from pynecore.lib import barmerge
|
|
1569
|
+
from pynecore.lib import timeframe as tf_module
|
|
1570
|
+
from .resampler import Resampler
|
|
1571
|
+
from .htf_aggregator import HTFAggregator
|
|
1572
|
+
|
|
1573
|
+
# Chart bar open -> last instant offset: multi-period boundaries resolve a
|
|
1574
|
+
# chart bar by the instant it ends at, so the bar containing a session
|
|
1575
|
+
# open counts as the new period's first bar. D/W/M chart bars are
|
|
1576
|
+
# session-aligned by construction and need no offset.
|
|
1577
|
+
# noinspection PyProtectedMember
|
|
1578
|
+
chart_mod, chart_mult = tf_module._process_tf(chart_timeframe)
|
|
1579
|
+
chart_off = (tf_module.in_seconds(chart_timeframe) * 1000 - 1
|
|
1580
|
+
if chart_mod in ('', 'S') else 0)
|
|
1581
|
+
|
|
1582
|
+
# Single-period civil daily/weekly/monthly chart: the LTF window cannot use
|
|
1583
|
+
# the (zero) ``chart_off`` span; ``__sec_signal__`` instead targets the
|
|
1584
|
+
# chart bar's civil period end via this resampler. Multi-period and
|
|
1585
|
+
# intraday charts keep the ``chart_off`` path. Only attached to LTF states.
|
|
1586
|
+
chart_ltf_resampler = None
|
|
1587
|
+
chart_ltf_modifier = ''
|
|
1588
|
+
if chart_mod in ('D', 'W', 'M') and chart_mult == 1:
|
|
1589
|
+
chart_ltf_resampler = Resampler.get_resampler(chart_timeframe)
|
|
1590
|
+
chart_ltf_modifier = chart_mod
|
|
1591
|
+
|
|
1592
|
+
sec_ids = list(contexts.keys())
|
|
1593
|
+
sync_block = SyncBlock(sec_ids)
|
|
1594
|
+
states: dict[str, SecurityState] = {}
|
|
1595
|
+
result_blocks: dict[str, ResultBlock] = {}
|
|
1596
|
+
|
|
1597
|
+
for sec_id, ctx in contexts.items():
|
|
1598
|
+
tf_val = ctx.get('timeframe', chart_timeframe)
|
|
1599
|
+
if tf_val is None or tf_val == '':
|
|
1600
|
+
# Runtime-dependent (deferred) timeframe gets the chart TF as a
|
|
1601
|
+
# placeholder until the runtime ``__sec_signal__`` resolves it;
|
|
1602
|
+
# an empty string IS the chart's timeframe (Pine semantics)
|
|
1603
|
+
tf_val = chart_timeframe
|
|
1604
|
+
timeframe = str(tf_val)
|
|
1605
|
+
is_ltf = bool(ctx.get('is_ltf', False))
|
|
1606
|
+
|
|
1607
|
+
htf_aggregator: HTFAggregator | None = None
|
|
1608
|
+
na_on_developing = False
|
|
1609
|
+
anchor_starts: 'list[SymInfoSession] | None' = None
|
|
1610
|
+
anchor_tz: ZoneInfo | None = None
|
|
1611
|
+
plain_ltf = False
|
|
1612
|
+
if is_ltf:
|
|
1613
|
+
is_gaps_on = False
|
|
1614
|
+
same_tf = False
|
|
1615
|
+
resampler = None # chart-side resampler not needed for LTF
|
|
1616
|
+
lookahead_mode = Lookahead.OFF # LTF has no lookahead concept
|
|
1617
|
+
else:
|
|
1618
|
+
gaps_val = ctx.get('gaps', barmerge.gaps_off)
|
|
1619
|
+
is_gaps_on = gaps_val is barmerge.gaps_on
|
|
1620
|
+
same_tf = (timeframe == chart_timeframe)
|
|
1621
|
+
# Plain security with a FINER timeframe than the chart: scalar
|
|
1622
|
+
# LTF merge (last/first intrabar of the chart bar), no resampler,
|
|
1623
|
+
# no HTF aggregator — the chart targets its own bar period.
|
|
1624
|
+
if not same_tf:
|
|
1625
|
+
sec_seconds = tf_module.in_seconds(timeframe)
|
|
1626
|
+
chart_seconds = tf_module.in_seconds(chart_timeframe)
|
|
1627
|
+
plain_ltf = 0 < sec_seconds < chart_seconds
|
|
1628
|
+
resampler = (None if same_tf or plain_ltf
|
|
1629
|
+
else Resampler.get_resampler(timeframe))
|
|
1630
|
+
|
|
1631
|
+
# A None value is a runtime-deferred (input-derived) lookahead; OFF
|
|
1632
|
+
# serves as the placeholder until the first ``__sec_signal__``
|
|
1633
|
+
# delivers the actual value.
|
|
1634
|
+
lookahead_mode = _lookahead_mode(ctx.get('lookahead'))
|
|
1635
|
+
|
|
1636
|
+
# Live HTF transport via the chart's ``HTFAggregator`` (closed-bar
|
|
1637
|
+
# override for all lookahead modes, plus developing-bar for
|
|
1638
|
+
# ``Lookahead.ON``) requires same-symbol chart→HTF aggregation.
|
|
1639
|
+
# The chart bar OHLCV must belong to the same instrument as the
|
|
1640
|
+
# security, so cross-symbol HTF keeps no aggregator: in backtest
|
|
1641
|
+
# it reads from the security's own ``.ohlcv`` file; in live mode
|
|
1642
|
+
# the security subprocess drives its own provider (warmup
|
|
1643
|
+
# download + WS stream) so the cross-symbol context advances on
|
|
1644
|
+
# real feed bars instead of staying inert.
|
|
1645
|
+
if not same_tf and resampler is not None:
|
|
1646
|
+
sym = ctx.get('symbol')
|
|
1647
|
+
if sym is not None:
|
|
1648
|
+
# Strip any chart-type marker (``ticker.heikinashi()``) so a
|
|
1649
|
+
# static same-symbol chart-type HTF resolves as same-symbol
|
|
1650
|
+
# (and gets an aggregator), not misrouted as cross-symbol.
|
|
1651
|
+
from ..lib.ticker import _split_chart_type
|
|
1652
|
+
sym, _ = _split_chart_type(str(sym))
|
|
1653
|
+
is_same_symbol = (
|
|
1654
|
+
chart_symbol is None
|
|
1655
|
+
or sym is None
|
|
1656
|
+
or str(sym) == chart_symbol
|
|
1657
|
+
)
|
|
1658
|
+
|
|
1659
|
+
# Intraday session anchoring: align HTF bars to the session open
|
|
1660
|
+
# (TradingView behaviour) when the open is off the requested tf
|
|
1661
|
+
# grid. Use the security's OWN syminfo — correct even for a
|
|
1662
|
+
# cross-symbol HTF in a different exchange session.
|
|
1663
|
+
si = (sec_syminfos.get(sec_id)
|
|
1664
|
+
if sec_syminfos is not None else None) or chart_syminfo
|
|
1665
|
+
anchor_starts, anchor_tz = resolve_session_anchor(si, timeframe, tz)
|
|
1666
|
+
|
|
1667
|
+
if is_same_symbol:
|
|
1668
|
+
htf_aggregator = HTFAggregator(
|
|
1669
|
+
timeframe, tz, session_starts=anchor_starts,
|
|
1670
|
+
chart_span_ms=chart_off + 1 if chart_off else 0)
|
|
1671
|
+
elif lookahead_mode is Lookahead.ON:
|
|
1672
|
+
# Cross-symbol HTF + lookahead_on: developing bar cannot
|
|
1673
|
+
# be aggregated from chart OHLCV (wrong instrument). The
|
|
1674
|
+
# subprocess still advances on closed cross-symbol HTF
|
|
1675
|
+
# bars, but the chart-side read returns ``na`` on every
|
|
1676
|
+
# chart bar inside an open HTF period — backtest never
|
|
1677
|
+
# silently exposes a value live could not produce, and
|
|
1678
|
+
# the ``close[1]`` idiom keeps working at the period
|
|
1679
|
+
# boundary.
|
|
1680
|
+
na_on_developing = True
|
|
1681
|
+
|
|
1682
|
+
state = SecurityState(
|
|
1683
|
+
sec_id=sec_id,
|
|
1684
|
+
timeframe=timeframe,
|
|
1685
|
+
gaps_on=is_gaps_on,
|
|
1686
|
+
same_timeframe=same_tf,
|
|
1687
|
+
resampler=resampler,
|
|
1688
|
+
tz=tz,
|
|
1689
|
+
is_ltf=is_ltf,
|
|
1690
|
+
plain_ltf=plain_ltf,
|
|
1691
|
+
plain_ltf_span_ms=(tf_module.in_seconds(timeframe) * 1000
|
|
1692
|
+
if plain_ltf else 0),
|
|
1693
|
+
lookahead=lookahead_mode,
|
|
1694
|
+
htf_aggregator=htf_aggregator,
|
|
1695
|
+
na_on_developing=na_on_developing,
|
|
1696
|
+
session_starts=anchor_starts,
|
|
1697
|
+
session_tz=anchor_tz,
|
|
1698
|
+
chart_off=chart_off,
|
|
1699
|
+
chart_resampler=chart_ltf_resampler if (is_ltf or plain_ltf) else None,
|
|
1700
|
+
chart_dwm_modifier=chart_ltf_modifier if (is_ltf or plain_ltf) else '',
|
|
1701
|
+
)
|
|
1702
|
+
# data_ready starts SET so reads before first signal return na (via result_size=0)
|
|
1703
|
+
state.data_ready.set()
|
|
1704
|
+
|
|
1705
|
+
states[sec_id] = state
|
|
1706
|
+
|
|
1707
|
+
result_block = ResultBlock(sec_id, create=True, version=0, size=INITIAL_RESULT_SIZE)
|
|
1708
|
+
result_blocks[sec_id] = result_block
|
|
1709
|
+
|
|
1710
|
+
return states, sync_block, result_blocks
|
|
1711
|
+
|
|
1712
|
+
|
|
1713
|
+
def inject_protocol(module, signal_fn, write_fn, read_fn, wait_fn,
|
|
1714
|
+
active_security=None,
|
|
1715
|
+
same_context: 'set[str] | frozenset[str]' = frozenset()):
|
|
1716
|
+
"""
|
|
1717
|
+
Inject protocol functions and __active_security__ into a script module's globals.
|
|
1718
|
+
|
|
1719
|
+
:param module: The script module
|
|
1720
|
+
:param signal_fn: __sec_signal__ implementation
|
|
1721
|
+
:param write_fn: __sec_write__ implementation
|
|
1722
|
+
:param read_fn: __sec_read__ implementation
|
|
1723
|
+
:param wait_fn: __sec_wait__ implementation
|
|
1724
|
+
:param active_security: None for chart context, sec_id for security context
|
|
1725
|
+
:param same_context: Frozenset of sec_ids sharing the chart's symbol+timeframe
|
|
1726
|
+
"""
|
|
1727
|
+
module.__sec_signal__ = signal_fn
|
|
1728
|
+
module.__sec_write__ = write_fn
|
|
1729
|
+
module.__sec_read__ = read_fn
|
|
1730
|
+
module.__sec_wait__ = wait_fn
|
|
1731
|
+
module.__active_security__ = active_security
|
|
1732
|
+
module.__same_context__ = same_context
|
|
1733
|
+
|
|
1734
|
+
|
|
1735
|
+
def cleanup_shared_memory(
|
|
1736
|
+
sync_block: SyncBlock,
|
|
1737
|
+
result_blocks: dict[str, ResultBlock],
|
|
1738
|
+
):
|
|
1739
|
+
"""
|
|
1740
|
+
Clean up all shared memory resources.
|
|
1741
|
+
|
|
1742
|
+
:param sync_block: The sync block to close and unlink
|
|
1743
|
+
:param result_blocks: Result blocks to close and unlink
|
|
1744
|
+
"""
|
|
1745
|
+
for rb in result_blocks.values():
|
|
1746
|
+
rb.close()
|
|
1747
|
+
rb.unlink()
|
|
1748
|
+
sync_block.close()
|
|
1749
|
+
sync_block.unlink()
|