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
|
File without changes
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OHLCV timeframe aggregation — converts lower timeframe data to higher timeframes.
|
|
3
|
+
|
|
4
|
+
Uses Resampler.get_bar_time() for correct bar boundary alignment across all
|
|
5
|
+
timeframe types including weekly and monthly. Multi-period targets (nD/nW/nM,
|
|
6
|
+
n > 1) live on the year-reset scheduled grid (see ``resampler`` module docs);
|
|
7
|
+
'observed' symbols (exchange-listed) count the actual trading days seen in the
|
|
8
|
+
source stream, which reproduces TradingView's holiday-aware grid.
|
|
9
|
+
"""
|
|
10
|
+
from datetime import timezone as dt_timezone
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from zoneinfo import ZoneInfo
|
|
13
|
+
|
|
14
|
+
from .ohlcv_file import OHLCVReader, OHLCVWriter
|
|
15
|
+
from .resampler import (
|
|
16
|
+
Resampler, ObservedDayCounter, grid_mode, overnight_opens, trading_day,
|
|
17
|
+
)
|
|
18
|
+
from ..lib.timeframe import in_seconds, _process_tf
|
|
19
|
+
from ..types.ohlcv import OHLCV
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def validate_aggregation(source_tf: str, target_tf: str) -> None:
|
|
23
|
+
"""
|
|
24
|
+
Validate that aggregation from source to target timeframe is possible.
|
|
25
|
+
|
|
26
|
+
:param source_tf: Source timeframe string (e.g., '5', '1D')
|
|
27
|
+
:param target_tf: Target timeframe string (e.g., '60', '1W')
|
|
28
|
+
:raises ValueError: If timeframes are incompatible
|
|
29
|
+
"""
|
|
30
|
+
source_sec = in_seconds(source_tf)
|
|
31
|
+
target_sec = in_seconds(target_tf)
|
|
32
|
+
|
|
33
|
+
if target_sec <= source_sec:
|
|
34
|
+
raise ValueError(
|
|
35
|
+
f"Target timeframe ({target_tf}) must be larger than "
|
|
36
|
+
f"source timeframe ({source_tf})"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
if target_sec % source_sec != 0:
|
|
40
|
+
raise ValueError(
|
|
41
|
+
f"Target timeframe ({target_tf}) must be evenly divisible by "
|
|
42
|
+
f"source timeframe ({source_tf})"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _merge_candles(candles: list[OHLCV], bar_time: int) -> OHLCV:
|
|
47
|
+
"""
|
|
48
|
+
Merge a window of candles into a single aggregated candle.
|
|
49
|
+
|
|
50
|
+
:param candles: Non-empty list of OHLCV candles belonging to the same bar
|
|
51
|
+
:param bar_time: Aligned bar opening timestamp in seconds
|
|
52
|
+
:return: Aggregated OHLCV candle
|
|
53
|
+
"""
|
|
54
|
+
return OHLCV(
|
|
55
|
+
timestamp=bar_time,
|
|
56
|
+
open=candles[0].open,
|
|
57
|
+
high=max(c.high for c in candles),
|
|
58
|
+
low=min(c.low for c in candles),
|
|
59
|
+
close=candles[-1].close,
|
|
60
|
+
volume=sum(c.volume for c in candles),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def aggregate_ohlcv(
|
|
65
|
+
source_path: Path,
|
|
66
|
+
target_path: Path,
|
|
67
|
+
target_tf: str,
|
|
68
|
+
tz: ZoneInfo | dt_timezone | None = None,
|
|
69
|
+
session_starts: list | None = None,
|
|
70
|
+
opening_hours: list | None = None,
|
|
71
|
+
sym_type: str | None = None,
|
|
72
|
+
source_tf: str | None = None,
|
|
73
|
+
) -> tuple[int, int]:
|
|
74
|
+
"""
|
|
75
|
+
Aggregate OHLCV data from a lower timeframe file to a higher timeframe file.
|
|
76
|
+
|
|
77
|
+
:param source_path: Path to source .ohlcv file
|
|
78
|
+
:param target_path: Path to target .ohlcv file (will be overwritten)
|
|
79
|
+
:param target_tf: Target timeframe string (e.g., '60', '1W')
|
|
80
|
+
:param tz: Timezone for day/week/month boundary alignment.
|
|
81
|
+
Should match the data's timezone (from TOML metadata).
|
|
82
|
+
:param session_starts: Per-trading-day primary opens for intraday session
|
|
83
|
+
anchoring and multi-period grids. When given, intraday bars align
|
|
84
|
+
to the session open (TradingView behaviour) instead of the UTC
|
|
85
|
+
clock; ``None`` keeps the pure clock-floor.
|
|
86
|
+
See :meth:`Resampler.get_bar_time`.
|
|
87
|
+
:param opening_hours: ``SymInfo.opening_hours`` — trading-day roll source
|
|
88
|
+
for multi-period (nD/nW/nM) grids.
|
|
89
|
+
:param sym_type: ``SymInfo.type`` for :func:`grid_mode` classification of
|
|
90
|
+
multi-period grids. Exchange-listed symbols ('observed' mode)
|
|
91
|
+
count the actual trading days present in the source data; for an
|
|
92
|
+
exact TradingView-matching grid their source data should reach
|
|
93
|
+
back to the year's first trading day (the in-year counter is
|
|
94
|
+
otherwise approximated from the weekday grid).
|
|
95
|
+
:param source_tf: Source timeframe string. On multi-period (nD/nW/nM)
|
|
96
|
+
targets an intraday source bar belongs to the trading day its
|
|
97
|
+
*last* instant falls into — the bar containing a session open
|
|
98
|
+
starts the new day even when its timestamp precedes the open
|
|
99
|
+
(see the ``resampler`` module docs).
|
|
100
|
+
:return: Tuple of (source_candles_read, target_candles_written)
|
|
101
|
+
"""
|
|
102
|
+
# noinspection PyProtectedMember
|
|
103
|
+
modifier, multiplier = _process_tf(target_tf)
|
|
104
|
+
mode = grid_mode(sym_type, opening_hours)
|
|
105
|
+
|
|
106
|
+
src_off = 0
|
|
107
|
+
fold = False
|
|
108
|
+
if modifier in ('D', 'W', 'M') and multiplier > 1 and source_tf:
|
|
109
|
+
# noinspection PyProtectedMember
|
|
110
|
+
src_mod, _ = _process_tf(source_tf)
|
|
111
|
+
if src_mod in ('', 'S'):
|
|
112
|
+
src_off = in_seconds(source_tf) - 1
|
|
113
|
+
# An intraday source carries the per-bar end instants the observed
|
|
114
|
+
# holiday half-day fold needs (a daily source is already folded).
|
|
115
|
+
fold = True
|
|
116
|
+
|
|
117
|
+
if modifier in ('D', 'W', 'M') and multiplier > 1 and mode == 'observed':
|
|
118
|
+
return _aggregate_observed(
|
|
119
|
+
source_path, target_path, modifier, multiplier, tz,
|
|
120
|
+
session_starts, opening_hours, src_off, fold)
|
|
121
|
+
|
|
122
|
+
resampler = Resampler.get_resampler(target_tf)
|
|
123
|
+
|
|
124
|
+
source_count = 0
|
|
125
|
+
target_count = 0
|
|
126
|
+
|
|
127
|
+
with OHLCVReader(source_path) as reader:
|
|
128
|
+
with OHLCVWriter(target_path, truncate=True) as writer:
|
|
129
|
+
window: list[OHLCV] = []
|
|
130
|
+
current_bar_time: int | None = None
|
|
131
|
+
|
|
132
|
+
start_ts = reader.start_timestamp
|
|
133
|
+
if start_ts is None:
|
|
134
|
+
if reader.size == 1:
|
|
135
|
+
# A single-record source has no derivable interval (the reader
|
|
136
|
+
# needs two timestamps to infer one), so ``start_timestamp`` is
|
|
137
|
+
# None and ``read_from`` yields nothing — yet that lone bar IS a
|
|
138
|
+
# whole target period and must be emitted. Floor its timestamp
|
|
139
|
+
# onto the target grid (exactly as the loop below does) so HTF
|
|
140
|
+
# confirmation (the ``bar_opens`` clamp) and
|
|
141
|
+
# ``request.security(.., time)`` see the period boundary, not the
|
|
142
|
+
# raw sub-bar instant.
|
|
143
|
+
only = reader.read(0)
|
|
144
|
+
only_bar_time = resampler.get_bar_time(
|
|
145
|
+
(only.timestamp + src_off) * 1000, tz=tz,
|
|
146
|
+
session_starts=session_starts,
|
|
147
|
+
opening_hours=opening_hours, mode=mode) // 1000
|
|
148
|
+
writer.write(_merge_candles([only], only_bar_time))
|
|
149
|
+
return 1, 1
|
|
150
|
+
return 0, 0
|
|
151
|
+
|
|
152
|
+
for candle in reader.read_from(start_ts):
|
|
153
|
+
source_count += 1
|
|
154
|
+
|
|
155
|
+
# Resampler works in ms, OHLCV timestamps are in seconds.
|
|
156
|
+
# src_off resolves multi-period bars by their last instant.
|
|
157
|
+
bar_time_ms = resampler.get_bar_time(
|
|
158
|
+
(candle.timestamp + src_off) * 1000, tz=tz,
|
|
159
|
+
session_starts=session_starts,
|
|
160
|
+
opening_hours=opening_hours, mode=mode)
|
|
161
|
+
bar_time = bar_time_ms // 1000
|
|
162
|
+
|
|
163
|
+
if current_bar_time is not None and bar_time != current_bar_time:
|
|
164
|
+
# New bar boundary — flush the window
|
|
165
|
+
writer.write(_merge_candles(window, current_bar_time))
|
|
166
|
+
target_count += 1
|
|
167
|
+
window = []
|
|
168
|
+
|
|
169
|
+
current_bar_time = bar_time
|
|
170
|
+
window.append(candle)
|
|
171
|
+
|
|
172
|
+
# Flush last window
|
|
173
|
+
if window and current_bar_time is not None:
|
|
174
|
+
writer.write(_merge_candles(window, current_bar_time))
|
|
175
|
+
target_count += 1
|
|
176
|
+
|
|
177
|
+
return source_count, target_count
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _aggregate_observed(
|
|
181
|
+
source_path: Path,
|
|
182
|
+
target_path: Path,
|
|
183
|
+
modifier: str,
|
|
184
|
+
multiplier: int,
|
|
185
|
+
tz: ZoneInfo | dt_timezone | None,
|
|
186
|
+
session_starts: list | None,
|
|
187
|
+
opening_hours: list | None,
|
|
188
|
+
src_off: int = 0,
|
|
189
|
+
fold: bool = False,
|
|
190
|
+
) -> tuple[int, int]:
|
|
191
|
+
"""
|
|
192
|
+
Multi-period aggregation for 'observed' symbols (exchange-listed).
|
|
193
|
+
|
|
194
|
+
TradingView's grid on these symbols counts its holiday calendar's scheduled
|
|
195
|
+
trading days; the actual daily data realizes that calendar, so counting the
|
|
196
|
+
trading days present in the source stream reproduces the grid (year-reset
|
|
197
|
+
counter, verified 100% on CME 2022+). Periods are stamped with their first
|
|
198
|
+
source candle — TradingView's stamp on these symbols is the period's first
|
|
199
|
+
actual session.
|
|
200
|
+
|
|
201
|
+
The first (partial) year's counter is seeded from the weekday grid: the
|
|
202
|
+
trading days between Jan 1 and the data start are not observable, so the
|
|
203
|
+
phase there is approximate. Data reaching back to a year start is exact
|
|
204
|
+
from that year on.
|
|
205
|
+
|
|
206
|
+
:param source_path: Path to source .ohlcv file
|
|
207
|
+
:param target_path: Path to target .ohlcv file (will be overwritten)
|
|
208
|
+
:param modifier: 'D', 'W' or 'M' (from ``_process_tf``)
|
|
209
|
+
:param multiplier: Period multiplier (> 1)
|
|
210
|
+
:param tz: Exchange timezone
|
|
211
|
+
:param session_starts: ``SymInfo.session_starts`` template
|
|
212
|
+
:param opening_hours: ``SymInfo.opening_hours`` template
|
|
213
|
+
:param src_off: Source bar open -> last instant offset in seconds; a bar
|
|
214
|
+
belongs to the trading day its last instant falls into
|
|
215
|
+
:param fold: Fold holiday half-days into the early-close day (intraday
|
|
216
|
+
source only; see :class:`ObservedDayCounter`)
|
|
217
|
+
:return: Tuple of (source_candles_read, target_candles_written)
|
|
218
|
+
"""
|
|
219
|
+
on = overnight_opens(opening_hours, session_starts)
|
|
220
|
+
|
|
221
|
+
source_count = 0
|
|
222
|
+
target_count = 0
|
|
223
|
+
|
|
224
|
+
with OHLCVReader(source_path) as reader:
|
|
225
|
+
with OHLCVWriter(target_path, truncate=True) as writer:
|
|
226
|
+
window: list[OHLCV] = []
|
|
227
|
+
window_start: int | None = None
|
|
228
|
+
group_key: tuple | None = None
|
|
229
|
+
counter = ObservedDayCounter(tz, opening_hours, fold=fold)
|
|
230
|
+
|
|
231
|
+
start_ts = reader.start_timestamp
|
|
232
|
+
if start_ts is None:
|
|
233
|
+
return 0, 0
|
|
234
|
+
|
|
235
|
+
for candle in reader.read_from(start_ts):
|
|
236
|
+
source_count += 1
|
|
237
|
+
td = trading_day(candle.timestamp + src_off, tz, on)
|
|
238
|
+
bar_end = candle.timestamp + src_off + 1 if fold else None
|
|
239
|
+
counter.ordinal(td, bar_end)
|
|
240
|
+
key = counter.key(modifier, multiplier)
|
|
241
|
+
|
|
242
|
+
if group_key is not None and key != group_key:
|
|
243
|
+
writer.write(_merge_candles(window, window_start))
|
|
244
|
+
target_count += 1
|
|
245
|
+
window = []
|
|
246
|
+
window_start = None
|
|
247
|
+
|
|
248
|
+
group_key = key
|
|
249
|
+
if window_start is None:
|
|
250
|
+
window_start = candle.timestamp
|
|
251
|
+
window.append(candle)
|
|
252
|
+
|
|
253
|
+
if window and window_start is not None:
|
|
254
|
+
writer.write(_merge_candles(window, window_start))
|
|
255
|
+
target_count += 1
|
|
256
|
+
|
|
257
|
+
return source_count, target_count
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Bar Magnifier — groups lower-timeframe OHLCV candles into chart-timeframe windows.
|
|
3
|
+
|
|
4
|
+
Used by ScriptRunner when use_bar_magnifier=true: the script sees aggregated chart-TF
|
|
5
|
+
bars, while the broker emulator processes orders against each sub-bar for accurate fills.
|
|
6
|
+
|
|
7
|
+
Multi-period chart timeframes (nD/nW/nM, n > 1) live on the year-reset scheduled
|
|
8
|
+
grid (see the ``resampler`` module docs); 'observed' symbols (exchange-listed)
|
|
9
|
+
count the actual trading days seen in the sub-bar stream, which reproduces
|
|
10
|
+
TradingView's holiday-aware grid.
|
|
11
|
+
"""
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from datetime import timezone as dt_timezone
|
|
14
|
+
from typing import Iterable, Iterator
|
|
15
|
+
from zoneinfo import ZoneInfo
|
|
16
|
+
|
|
17
|
+
from .aggregator import _merge_candles
|
|
18
|
+
from .resampler import (
|
|
19
|
+
Resampler, ObservedDayCounter, grid_mode, overnight_opens, trading_day,
|
|
20
|
+
)
|
|
21
|
+
from ..lib.timeframe import in_seconds, _process_tf
|
|
22
|
+
from ..types.ohlcv import OHLCV
|
|
23
|
+
|
|
24
|
+
__all__ = ['BarMagnifier', 'MagnifiedWindow']
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(slots=True)
|
|
28
|
+
class MagnifiedWindow:
|
|
29
|
+
"""A single chart-timeframe bar with its constituent sub-bars."""
|
|
30
|
+
sub_bars: list[OHLCV]
|
|
31
|
+
aggregated: OHLCV
|
|
32
|
+
is_last_window: bool
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class BarMagnifier:
|
|
36
|
+
"""
|
|
37
|
+
Groups sub-timeframe OHLCV candles into chart-timeframe windows.
|
|
38
|
+
|
|
39
|
+
Uses Resampler for bar boundary alignment (same logic as aggregator.py);
|
|
40
|
+
multi-period (nD/nW/nM) chart timeframes on 'observed' symbols count the
|
|
41
|
+
actual trading days in the stream instead. Yields MagnifiedWindow objects
|
|
42
|
+
with peek-ahead for last-window detection.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
ohlcv_iter: Iterable[OHLCV],
|
|
48
|
+
chart_tf: str,
|
|
49
|
+
tz: ZoneInfo | dt_timezone | None = None,
|
|
50
|
+
session_starts: 'list | None' = None,
|
|
51
|
+
opening_hours: 'list | None' = None,
|
|
52
|
+
sym_type: str | None = None,
|
|
53
|
+
source_tf: str | None = None,
|
|
54
|
+
):
|
|
55
|
+
"""
|
|
56
|
+
:param ohlcv_iter: Iterator of sub-timeframe OHLCV candles
|
|
57
|
+
:param chart_tf: Chart timeframe string (e.g., '60', '1D')
|
|
58
|
+
:param tz: Timezone for day/week/month boundary alignment
|
|
59
|
+
:param session_starts: Per-trading-day primary opens for intraday session
|
|
60
|
+
anchoring and multi-period grids. ``None`` keeps the pure clock-floor
|
|
61
|
+
(see :meth:`Resampler.get_bar_time`).
|
|
62
|
+
:param opening_hours: ``SymInfo.opening_hours`` — trading-day roll source
|
|
63
|
+
for multi-period (nD/nW/nM) grids.
|
|
64
|
+
:param sym_type: ``SymInfo.type`` for :func:`grid_mode` classification of
|
|
65
|
+
multi-period grids.
|
|
66
|
+
:param source_tf: Sub-bar timeframe string. On multi-period chart
|
|
67
|
+
timeframes an intraday sub-bar belongs to the trading day its *last*
|
|
68
|
+
instant falls into — the bar containing a session open starts the
|
|
69
|
+
new day even when its timestamp precedes the open.
|
|
70
|
+
"""
|
|
71
|
+
self._ohlcv_iter = ohlcv_iter
|
|
72
|
+
self._resampler = Resampler.get_resampler(chart_tf)
|
|
73
|
+
self._tz = tz
|
|
74
|
+
self._session_starts = session_starts
|
|
75
|
+
self._opening_hours = opening_hours
|
|
76
|
+
|
|
77
|
+
# noinspection PyProtectedMember
|
|
78
|
+
modifier, multiplier = _process_tf(chart_tf)
|
|
79
|
+
self._modifier = modifier
|
|
80
|
+
self._multiplier = multiplier
|
|
81
|
+
multi = modifier in ('D', 'W', 'M') and multiplier > 1
|
|
82
|
+
self._mode = grid_mode(sym_type, opening_hours) if multi else None
|
|
83
|
+
|
|
84
|
+
self._src_off = 0
|
|
85
|
+
self._fold = False
|
|
86
|
+
if multi and source_tf:
|
|
87
|
+
# noinspection PyProtectedMember
|
|
88
|
+
src_mod, _ = _process_tf(source_tf)
|
|
89
|
+
if src_mod in ('', 'S'):
|
|
90
|
+
self._src_off = in_seconds(source_tf) - 1
|
|
91
|
+
# Intraday sub-bars carry the end instants the holiday half-day
|
|
92
|
+
# fold needs (a daily source stream is already folded).
|
|
93
|
+
self._fold = True
|
|
94
|
+
|
|
95
|
+
if multi and self._mode == 'observed':
|
|
96
|
+
self._overnight = overnight_opens(opening_hours, session_starts)
|
|
97
|
+
self._counter: ObservedDayCounter | None = ObservedDayCounter(
|
|
98
|
+
tz, opening_hours, fold=self._fold)
|
|
99
|
+
else:
|
|
100
|
+
self._overnight = {}
|
|
101
|
+
self._counter = None
|
|
102
|
+
|
|
103
|
+
def _key_and_stamp(self, candle: OHLCV) -> tuple[object, int]:
|
|
104
|
+
"""
|
|
105
|
+
Grouping key and window-opening timestamp (seconds) for a sub-bar.
|
|
106
|
+
|
|
107
|
+
For 'observed' multi-period grids the key counts the actual trading
|
|
108
|
+
days and the window is stamped by its first sub-bar; everything else
|
|
109
|
+
uses the scheduled-grid bar time as both.
|
|
110
|
+
"""
|
|
111
|
+
if self._counter is not None:
|
|
112
|
+
td = trading_day(candle.timestamp + self._src_off, self._tz, self._overnight)
|
|
113
|
+
bar_end = candle.timestamp + self._src_off + 1 if self._fold else None
|
|
114
|
+
self._counter.ordinal(td, bar_end)
|
|
115
|
+
key = self._counter.key(self._modifier, self._multiplier)
|
|
116
|
+
return key, candle.timestamp
|
|
117
|
+
|
|
118
|
+
bar_time_ms = self._resampler.get_bar_time(
|
|
119
|
+
(candle.timestamp + self._src_off) * 1000, tz=self._tz,
|
|
120
|
+
session_starts=self._session_starts,
|
|
121
|
+
opening_hours=self._opening_hours, mode=self._mode)
|
|
122
|
+
bar_time = bar_time_ms // 1000
|
|
123
|
+
return bar_time, bar_time
|
|
124
|
+
|
|
125
|
+
def __iter__(self) -> Iterator[MagnifiedWindow]:
|
|
126
|
+
window: list[OHLCV] = []
|
|
127
|
+
current_key: object | None = None
|
|
128
|
+
window_stamp: int | None = None
|
|
129
|
+
next_window: MagnifiedWindow | None = None
|
|
130
|
+
|
|
131
|
+
for candle in self._ohlcv_iter:
|
|
132
|
+
key, stamp = self._key_and_stamp(candle)
|
|
133
|
+
|
|
134
|
+
if current_key is not None and key != current_key:
|
|
135
|
+
# New bar boundary — flush current window
|
|
136
|
+
new_window = MagnifiedWindow(
|
|
137
|
+
sub_bars=window,
|
|
138
|
+
aggregated=_merge_candles(window, window_stamp),
|
|
139
|
+
is_last_window=False,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Peek-ahead: yield the previous window (now we know it's not the last)
|
|
143
|
+
if next_window is not None:
|
|
144
|
+
yield next_window
|
|
145
|
+
next_window = new_window
|
|
146
|
+
window = []
|
|
147
|
+
window_stamp = None
|
|
148
|
+
|
|
149
|
+
current_key = key
|
|
150
|
+
if window_stamp is None:
|
|
151
|
+
window_stamp = stamp
|
|
152
|
+
window.append(candle)
|
|
153
|
+
|
|
154
|
+
# Flush last window
|
|
155
|
+
if window and window_stamp is not None:
|
|
156
|
+
last_window = MagnifiedWindow(
|
|
157
|
+
sub_bars=window,
|
|
158
|
+
aggregated=_merge_candles(window, window_stamp),
|
|
159
|
+
is_last_window=True,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
if next_window is not None:
|
|
163
|
+
yield next_window
|
|
164
|
+
yield last_window
|
|
165
|
+
elif next_window is not None:
|
|
166
|
+
# Edge case: no trailing candles, previous window is the last
|
|
167
|
+
next_window.is_last_window = True
|
|
168
|
+
yield next_window
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Broker plugin runtime support.
|
|
3
|
+
|
|
4
|
+
- :mod:`pynecore.core.broker.models` — intent, event, exchange-state,
|
|
5
|
+
capability and requirement dataclasses.
|
|
6
|
+
- :mod:`pynecore.core.broker.exceptions` — broker error hierarchy.
|
|
7
|
+
- :mod:`pynecore.core.broker.position` — :class:`BrokerPosition` live
|
|
8
|
+
position tracker (no simulation).
|
|
9
|
+
"""
|
|
10
|
+
from pynecore.core.broker.exceptions import (
|
|
11
|
+
AuthenticationError,
|
|
12
|
+
BrokerError,
|
|
13
|
+
ExchangeCapabilityError,
|
|
14
|
+
ExchangeConnectionError,
|
|
15
|
+
ExchangeOrderRejectedError,
|
|
16
|
+
ExchangeRateLimitError,
|
|
17
|
+
InsufficientMarginError,
|
|
18
|
+
OrderSyncError,
|
|
19
|
+
UnexpectedCancelError,
|
|
20
|
+
)
|
|
21
|
+
from pynecore.core.broker.models import (
|
|
22
|
+
OrderStatus,
|
|
23
|
+
OrderType,
|
|
24
|
+
LegType,
|
|
25
|
+
CapabilityLevel,
|
|
26
|
+
ExchangeOrder,
|
|
27
|
+
OrderEvent,
|
|
28
|
+
ExchangePosition,
|
|
29
|
+
ExchangeCapabilities,
|
|
30
|
+
EntryIntent,
|
|
31
|
+
ExitIntent,
|
|
32
|
+
CloseIntent,
|
|
33
|
+
CancelIntent,
|
|
34
|
+
ScriptRequirements,
|
|
35
|
+
InterceptorResult,
|
|
36
|
+
)
|
|
37
|
+
from pynecore.core.broker.position import BrokerPosition
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
'AuthenticationError',
|
|
41
|
+
'BrokerError',
|
|
42
|
+
'ExchangeCapabilityError',
|
|
43
|
+
'ExchangeConnectionError',
|
|
44
|
+
'ExchangeOrderRejectedError',
|
|
45
|
+
'ExchangeRateLimitError',
|
|
46
|
+
'InsufficientMarginError',
|
|
47
|
+
'OrderSyncError',
|
|
48
|
+
'UnexpectedCancelError',
|
|
49
|
+
'OrderStatus',
|
|
50
|
+
'OrderType',
|
|
51
|
+
'LegType',
|
|
52
|
+
'CapabilityLevel',
|
|
53
|
+
'ExchangeOrder',
|
|
54
|
+
'OrderEvent',
|
|
55
|
+
'ExchangePosition',
|
|
56
|
+
'ExchangeCapabilities',
|
|
57
|
+
'EntryIntent',
|
|
58
|
+
'ExitIntent',
|
|
59
|
+
'CloseIntent',
|
|
60
|
+
'CancelIntent',
|
|
61
|
+
'ScriptRequirements',
|
|
62
|
+
'InterceptorResult',
|
|
63
|
+
'BrokerPosition',
|
|
64
|
+
]
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cross-broker runtime defaults loaded from ``workdir/config/brokers.toml``.
|
|
3
|
+
|
|
4
|
+
Holds policies that are broker-agnostic by design — the four
|
|
5
|
+
``on_unexpected_cancel`` modes, for instance, share identical semantics
|
|
6
|
+
regardless of which exchange the plugin talks to. Living here rather than
|
|
7
|
+
in each plugin's own config keeps the user-facing knob in a single place
|
|
8
|
+
and prevents every new broker plugin from copy-pasting the same field.
|
|
9
|
+
|
|
10
|
+
The CLI (``pyne run --broker``) loads :class:`BrokerDefaults` once and
|
|
11
|
+
injects the resolved values onto the plugin instance just before the
|
|
12
|
+
script runner starts. Plugin code reads them through the
|
|
13
|
+
:class:`~pynecore.core.plugin.broker.BrokerPlugin` class attributes that
|
|
14
|
+
they shadow.
|
|
15
|
+
"""
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from pynecore.core.config import ensure_config
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
'BrokerDefaults',
|
|
23
|
+
'VALID_UNEXPECTED_CANCEL_POLICIES',
|
|
24
|
+
'VALID_INVENTORY_CONFLICT_POLICIES',
|
|
25
|
+
'load_broker_defaults',
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
VALID_UNEXPECTED_CANCEL_POLICIES = frozenset({
|
|
30
|
+
"stop",
|
|
31
|
+
"stop_and_cancel",
|
|
32
|
+
"re_place",
|
|
33
|
+
"ignore",
|
|
34
|
+
"halt",
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
VALID_INVENTORY_CONFLICT_POLICIES = frozenset({
|
|
38
|
+
"quarantine",
|
|
39
|
+
"halt",
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class BrokerDefaults:
|
|
45
|
+
"""Cross-broker runtime defaults.
|
|
46
|
+
|
|
47
|
+
Loaded from ``workdir/config/brokers.toml`` via
|
|
48
|
+
:func:`load_broker_defaults`. The file is self-healing — fields at
|
|
49
|
+
their default are emitted as commented-out lines, user-edited values
|
|
50
|
+
are preserved across regenerations.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
on_unexpected_cancel: str = "stop"
|
|
54
|
+
"""Policy when a bot-owned order disappears without the bot cancelling it.
|
|
55
|
+
|
|
56
|
+
``"stop"`` (default) — quarantine: trading stops (no new or
|
|
57
|
+
exposure-increasing dispatch) but the process stays alive — event
|
|
58
|
+
ingestion, cancels and closes keep working and observability can
|
|
59
|
+
page. Resumed by an operator restart.
|
|
60
|
+
``"stop_and_cancel"`` — quarantine plus a best-effort cancel pass
|
|
61
|
+
over the remaining bot-owned orders.
|
|
62
|
+
``"re_place"`` — no-op on the cancel; the sync engine re-dispatches
|
|
63
|
+
the protective order on the next diff cycle.
|
|
64
|
+
``"ignore"`` — silently continue. Only safe when manual external
|
|
65
|
+
cancellations are an expected part of the operational workflow.
|
|
66
|
+
``"halt"`` — exit the process via the graceful manual-intervention
|
|
67
|
+
path, leaving any remaining orders unsupervised until restart.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
on_inventory_conflict: str = "quarantine"
|
|
71
|
+
"""Policy for a confirmed spot balance-invariant conflict.
|
|
72
|
+
|
|
73
|
+
Only used by plugins that opt into the core spot inventory layer.
|
|
74
|
+
``"quarantine"`` (default) — trading stops, the process stays alive
|
|
75
|
+
as an observer; recovery is an operator rebaseline plus restart.
|
|
76
|
+
``"halt"`` — exit via the graceful manual-intervention path.
|
|
77
|
+
The set is deliberately narrower than ``on_unexpected_cancel``:
|
|
78
|
+
an attribution conflict has no safe ``re_place`` or ``ignore``
|
|
79
|
+
analogue (the former would buy back an operator's withdrawal, the
|
|
80
|
+
latter would trade on corrupt books).
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def load_broker_defaults(config_dir: Path) -> BrokerDefaults:
|
|
85
|
+
"""Load :class:`BrokerDefaults` from ``<config_dir>/brokers.toml``.
|
|
86
|
+
|
|
87
|
+
Delegates to :func:`pynecore.core.config.ensure_config`, which
|
|
88
|
+
auto-creates the file with commented defaults on first run, preserves
|
|
89
|
+
user-edited values across regenerations, and caches the result on the
|
|
90
|
+
dataclass. Validation runs after loading — invalid values raise
|
|
91
|
+
:class:`ValueError` with the list of accepted policies so the
|
|
92
|
+
misconfiguration surfaces immediately at startup, not at the first
|
|
93
|
+
reconcile cycle.
|
|
94
|
+
|
|
95
|
+
:param config_dir: The ``workdir/config`` directory.
|
|
96
|
+
:return: A populated :class:`BrokerDefaults` instance.
|
|
97
|
+
:raises ValueError: If a loaded value falls outside its allowed set.
|
|
98
|
+
"""
|
|
99
|
+
instance = ensure_config(BrokerDefaults, config_dir / 'brokers.toml')
|
|
100
|
+
assert isinstance(instance, BrokerDefaults)
|
|
101
|
+
if instance.on_unexpected_cancel not in VALID_UNEXPECTED_CANCEL_POLICIES:
|
|
102
|
+
raise ValueError(
|
|
103
|
+
f"brokers.toml: on_unexpected_cancel must be one of "
|
|
104
|
+
f"{sorted(VALID_UNEXPECTED_CANCEL_POLICIES)}, got "
|
|
105
|
+
f"{instance.on_unexpected_cancel!r}",
|
|
106
|
+
)
|
|
107
|
+
if instance.on_inventory_conflict not in VALID_INVENTORY_CONFLICT_POLICIES:
|
|
108
|
+
raise ValueError(
|
|
109
|
+
f"brokers.toml: on_inventory_conflict must be one of "
|
|
110
|
+
f"{sorted(VALID_INVENTORY_CONFLICT_POLICIES)}, got "
|
|
111
|
+
f"{instance.on_inventory_conflict!r}",
|
|
112
|
+
)
|
|
113
|
+
return instance
|