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,401 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Canonical ``client_order_id`` formula for BrokerPlugin dispatches.
|
|
3
|
+
|
|
4
|
+
The broker layer uses a deterministic identifier so that retries, reconnects
|
|
5
|
+
and even full process restarts converge on the same exchange-side client
|
|
6
|
+
order id — idempotent by construction.
|
|
7
|
+
|
|
8
|
+
The format is a 30-character budget, chosen as the lowest common denominator
|
|
9
|
+
across supported exchanges: Capital.com ``dealReference`` is ≤ 30 chars;
|
|
10
|
+
Binance ``newClientOrderId``, Bybit ``orderLinkId``, OKX ``clOrdId``,
|
|
11
|
+
Interactive Brokers ``orderRef`` and Deribit ``label`` all accept at least
|
|
12
|
+
that many characters. A single string therefore fits every plugin without
|
|
13
|
+
per-exchange branching.
|
|
14
|
+
|
|
15
|
+
Format::
|
|
16
|
+
|
|
17
|
+
{run}-{pid}-{bar}-{k}{r}
|
|
18
|
+
|
|
19
|
+
===== ========== ============================================================
|
|
20
|
+
Field Width Content
|
|
21
|
+
===== ========== ============================================================
|
|
22
|
+
run 4 base36 Session-stable hash of the script source / config.
|
|
23
|
+
pid 8 base36 Hash of the Pine-level order id (``pine_id``).
|
|
24
|
+
bar 9 base36 Bar open timestamp in milliseconds since the Unix epoch.
|
|
25
|
+
k 1 Single-character kind code (entry / TP / SL / close / cancel).
|
|
26
|
+
r 1–2 base36 Retry sequence — normally ``0``; bumped only when a prior
|
|
27
|
+
attempt is deliberately abandoned.
|
|
28
|
+
===== ========== ============================================================
|
|
29
|
+
|
|
30
|
+
Every field but ``r`` is fixed-width, keeping the result deterministic across
|
|
31
|
+
processes and Python versions. The formula is **pure**: two dispatches for the
|
|
32
|
+
same logical order on the same bar always produce identical ids. Exchanges
|
|
33
|
+
that enforce client-id uniqueness reject the duplicate outright; exchanges
|
|
34
|
+
that do not (Interactive Brokers, Deribit) dedup inside the plugin via a
|
|
35
|
+
``get_open_orders`` match on the same id.
|
|
36
|
+
|
|
37
|
+
Wire form for short-budget venues
|
|
38
|
+
---------------------------------
|
|
39
|
+
|
|
40
|
+
A venue whose client-id limit is below the canonical width (some FIX
|
|
41
|
+
implementations cap ``ClOrdID`` at 20 characters) cannot carry the canonical
|
|
42
|
+
id. For those, :func:`encode_wire_client_order_id` derives a fixed-length
|
|
43
|
+
**wire form** that exactly fills the venue budget declared by the plugin
|
|
44
|
+
(:attr:`~pynecore.core.plugin.broker.BrokerPlugin.client_order_id_max_len`)::
|
|
45
|
+
|
|
46
|
+
{run}{bar}{k}{hash}
|
|
47
|
+
|
|
48
|
+
===== ================== ====================================================
|
|
49
|
+
Field Width Content
|
|
50
|
+
===== ================== ====================================================
|
|
51
|
+
run 4 base36 Same session tag as the canonical form, raw.
|
|
52
|
+
bar 9 base36 Same bar open timestamp (ms), raw.
|
|
53
|
+
k 1 Same single-character kind code, raw.
|
|
54
|
+
hash budget - 14 base36 sha256 of the FULL canonical id, base36-encoded.
|
|
55
|
+
===== ================== ====================================================
|
|
56
|
+
|
|
57
|
+
The three raw fields keep the restart adoption path cheap: a wire id echoed
|
|
58
|
+
by the broker still reveals *whose run*, *which bar* and *which kind* it is,
|
|
59
|
+
so recognising a lost anchor only has to forward-hash ``retry_seq``
|
|
60
|
+
candidates for a known ``pine_id`` and compare against the opaque tail. The
|
|
61
|
+
tail simultaneously carries the hashed ``pid`` / ``retry`` identity and
|
|
62
|
+
confirms the full canonical match — an equal wire string is an equal logical
|
|
63
|
+
order (modulo a >=31-bit hash collision; :data:`WIRE_CLIENT_ORDER_ID_MIN_LEN`
|
|
64
|
+
floors the budget at 20 so the tail never drops below 6 characters).
|
|
65
|
+
|
|
66
|
+
The mapping is deterministic and applied at every mint site through
|
|
67
|
+
:meth:`~pynecore.core.broker.models.DispatchEnvelope.client_order_id`, so
|
|
68
|
+
journal rows, broker echoes and rebuilt references all agree on the wire
|
|
69
|
+
form. A canonical id that already fits the budget is passed through
|
|
70
|
+
unchanged — venues accepting >= 30 characters are byte-for-byte unaffected.
|
|
71
|
+
"""
|
|
72
|
+
from __future__ import annotations
|
|
73
|
+
|
|
74
|
+
import dataclasses
|
|
75
|
+
import hashlib
|
|
76
|
+
from typing import Final
|
|
77
|
+
|
|
78
|
+
__all__ = [
|
|
79
|
+
'ParsedClientOrderId',
|
|
80
|
+
'ParsedWireClientOrderId',
|
|
81
|
+
'parse_client_order_id',
|
|
82
|
+
'parse_wire_client_order_id',
|
|
83
|
+
'encode_wire_client_order_id',
|
|
84
|
+
'WIRE_CLIENT_ORDER_ID_MIN_LEN',
|
|
85
|
+
'WIRE_RAW_PREFIX_LEN',
|
|
86
|
+
'KIND_ENTRY',
|
|
87
|
+
'KIND_ENTRY_STOP',
|
|
88
|
+
'KIND_ENTRY_STOP_WATCH',
|
|
89
|
+
'KIND_EXIT_TP',
|
|
90
|
+
'KIND_EXIT_SL',
|
|
91
|
+
'KIND_EXIT_TP_PARTIAL',
|
|
92
|
+
'KIND_EXIT_SL_PARTIAL',
|
|
93
|
+
'KIND_EXIT_TRAIL_PARTIAL',
|
|
94
|
+
'KIND_CLOSE',
|
|
95
|
+
'KIND_CANCEL',
|
|
96
|
+
'KIND_MODIFY_ENTRY',
|
|
97
|
+
'KIND_MODIFY_EXIT',
|
|
98
|
+
'VALID_KINDS',
|
|
99
|
+
'CLIENT_ORDER_ID_MAX_LEN',
|
|
100
|
+
'RUN_TAG_WIDTH',
|
|
101
|
+
'PINE_ID_HASH_WIDTH',
|
|
102
|
+
'BAR_TS_WIDTH',
|
|
103
|
+
'build_client_order_id',
|
|
104
|
+
'hash_pine_id',
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
# === Kind codes (single-character) =======================================
|
|
108
|
+
|
|
109
|
+
KIND_ENTRY: Final[str] = 'e'
|
|
110
|
+
# Market entry fired by the software price-watch on the STOP side of a both-set
|
|
111
|
+
# Pine entry (``strategy.entry(limit=, stop=)``). Distinct from KIND_ENTRY so
|
|
112
|
+
# the stop-fired MARKET and the native LIMIT leg of the same ``pine_id`` get
|
|
113
|
+
# different client-order-ids — the engine persists this id before the POST so a
|
|
114
|
+
# crash-restart can verify-before-resend and never double-open.
|
|
115
|
+
KIND_ENTRY_STOP: Final[str] = 'b'
|
|
116
|
+
# Storage-only client-order-id for the engine-internal entry-stop WATCH row
|
|
117
|
+
# (no exchange order — the software state machine owns it, mirroring the
|
|
118
|
+
# partial-bracket leg rows). Distinct from KIND_ENTRY_STOP ('b'), which is the
|
|
119
|
+
# actual stop-fired MARKET order, so the watch row and the market order never
|
|
120
|
+
# share an ``orders`` table primary key.
|
|
121
|
+
KIND_ENTRY_STOP_WATCH: Final[str] = 'w'
|
|
122
|
+
KIND_EXIT_TP: Final[str] = 't'
|
|
123
|
+
KIND_EXIT_SL: Final[str] = 's'
|
|
124
|
+
# Engine-trigger partial bracket leg kinds. Distinct lowercase codes —
|
|
125
|
+
# uppercase variants would collide with the native TP / SL / modify-exit
|
|
126
|
+
# codes on exchanges that case-normalise client ids, so the partial-bracket
|
|
127
|
+
# legs (which own no exchange-side order while armed) get their own letters.
|
|
128
|
+
KIND_EXIT_TP_PARTIAL: Final[str] = 'p'
|
|
129
|
+
KIND_EXIT_SL_PARTIAL: Final[str] = 'q'
|
|
130
|
+
KIND_EXIT_TRAIL_PARTIAL: Final[str] = 'l'
|
|
131
|
+
KIND_CLOSE: Final[str] = 'c'
|
|
132
|
+
KIND_CANCEL: Final[str] = 'x'
|
|
133
|
+
KIND_MODIFY_ENTRY: Final[str] = 'n'
|
|
134
|
+
KIND_MODIFY_EXIT: Final[str] = 'r'
|
|
135
|
+
|
|
136
|
+
VALID_KINDS: Final[frozenset[str]] = frozenset({
|
|
137
|
+
KIND_ENTRY, KIND_ENTRY_STOP, KIND_ENTRY_STOP_WATCH,
|
|
138
|
+
KIND_EXIT_TP, KIND_EXIT_SL,
|
|
139
|
+
KIND_EXIT_TP_PARTIAL, KIND_EXIT_SL_PARTIAL, KIND_EXIT_TRAIL_PARTIAL,
|
|
140
|
+
KIND_CLOSE, KIND_CANCEL,
|
|
141
|
+
KIND_MODIFY_ENTRY, KIND_MODIFY_EXIT,
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
# === Width constants =====================================================
|
|
145
|
+
|
|
146
|
+
CLIENT_ORDER_ID_MAX_LEN: Final[int] = 30
|
|
147
|
+
RUN_TAG_WIDTH: Final[int] = 4
|
|
148
|
+
PINE_ID_HASH_WIDTH: Final[int] = 8
|
|
149
|
+
BAR_TS_WIDTH: Final[int] = 9
|
|
150
|
+
|
|
151
|
+
# Wire form: {run4}{bar9}{kind1} raw prefix + base36 sha256 tail (module
|
|
152
|
+
# docstring, "Wire form for short-budget venues").
|
|
153
|
+
WIRE_RAW_PREFIX_LEN: Final[int] = RUN_TAG_WIDTH + BAR_TS_WIDTH + 1
|
|
154
|
+
# Budget floor: 14 raw + >=6 hash chars (~31 bits). Below that the tail gets
|
|
155
|
+
# too weak to confirm identity on the restart adoption path.
|
|
156
|
+
WIRE_CLIENT_ORDER_ID_MIN_LEN: Final[int] = 20
|
|
157
|
+
|
|
158
|
+
# === Base36 encoding =====================================================
|
|
159
|
+
|
|
160
|
+
_BASE36_DIGITS: Final[str] = '0123456789abcdefghijklmnopqrstuvwxyz'
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _to_base36(value: int, *, width: int | None = None) -> str:
|
|
164
|
+
"""Encode a non-negative integer in lower-case base36.
|
|
165
|
+
|
|
166
|
+
:param value: Non-negative integer to encode.
|
|
167
|
+
:param width: When given, left-pad with ``'0'`` to this width. The
|
|
168
|
+
function never truncates — an encoded value wider than ``width``
|
|
169
|
+
raises :class:`ValueError`, because silent truncation would break
|
|
170
|
+
determinism on overflow.
|
|
171
|
+
:raises ValueError: On negative input or width overflow.
|
|
172
|
+
"""
|
|
173
|
+
if value < 0:
|
|
174
|
+
raise ValueError(f"value must be non-negative, got {value}")
|
|
175
|
+
if value == 0:
|
|
176
|
+
encoded = '0'
|
|
177
|
+
else:
|
|
178
|
+
digits: list[str] = []
|
|
179
|
+
n = value
|
|
180
|
+
while n:
|
|
181
|
+
digits.append(_BASE36_DIGITS[n % 36])
|
|
182
|
+
n //= 36
|
|
183
|
+
encoded = ''.join(reversed(digits))
|
|
184
|
+
if width is not None:
|
|
185
|
+
if len(encoded) > width:
|
|
186
|
+
raise ValueError(
|
|
187
|
+
f"encoded value {encoded!r} exceeds requested width {width}",
|
|
188
|
+
)
|
|
189
|
+
encoded = encoded.rjust(width, '0')
|
|
190
|
+
return encoded
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# === Public helpers ======================================================
|
|
194
|
+
|
|
195
|
+
def hash_pine_id(pine_id: str) -> str:
|
|
196
|
+
"""Return an 8-character base36 hash of a Pine-level order id.
|
|
197
|
+
|
|
198
|
+
Pine ids can contain arbitrary characters (spaces, slashes, unicode) and
|
|
199
|
+
arbitrary lengths; hashing fits the budget and neutralises odd input.
|
|
200
|
+
40 bits of sha256 output are encoded, yielding ~1.1e12 distinct slots
|
|
201
|
+
before a birthday collision becomes plausible — orders of magnitude above
|
|
202
|
+
any realistic per-strategy id count.
|
|
203
|
+
|
|
204
|
+
:param pine_id: The Pine order identifier (e.g. ``"Long"``, ``"TP/Long"``).
|
|
205
|
+
:return: Exactly 8 lower-case base36 characters.
|
|
206
|
+
"""
|
|
207
|
+
digest = hashlib.sha256(pine_id.encode('utf-8')).digest()
|
|
208
|
+
# 40 bits → ceil(log36(2**40)) == 8 chars. A 41st bit would overflow 8.
|
|
209
|
+
value = int.from_bytes(digest[:5], 'big')
|
|
210
|
+
return _to_base36(value, width=PINE_ID_HASH_WIDTH)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def build_client_order_id(
|
|
214
|
+
*,
|
|
215
|
+
run_tag: str,
|
|
216
|
+
pine_id: str,
|
|
217
|
+
bar_ts_ms: int,
|
|
218
|
+
kind: str,
|
|
219
|
+
retry_seq: int = 0,
|
|
220
|
+
) -> str:
|
|
221
|
+
"""Build the canonical client-order-id for a broker dispatch.
|
|
222
|
+
|
|
223
|
+
:param run_tag: 4-char base36 session tag (see
|
|
224
|
+
:meth:`~pynecore.core.broker.run_identity.RunIdentity.make_run_tag`).
|
|
225
|
+
:param pine_id: Pine-level order identifier; hashed internally.
|
|
226
|
+
:param bar_ts_ms: Bar open timestamp in milliseconds since the Unix epoch.
|
|
227
|
+
Must be non-negative.
|
|
228
|
+
:param kind: One of the single-character codes in
|
|
229
|
+
:data:`VALID_KINDS` (entry / TP / SL / close / cancel /
|
|
230
|
+
modify-entry / modify-exit).
|
|
231
|
+
:param retry_seq: Bumped only when the sync engine deliberately abandons
|
|
232
|
+
a prior attempt (e.g. the exchange never acknowledged the original
|
|
233
|
+
dispatch and the recovery timeout expired). ``0`` by default.
|
|
234
|
+
:raises ValueError: On malformed ``run_tag`` / ``kind``, negative ``bar_ts_ms``
|
|
235
|
+
/ ``retry_seq``, or when the formatted id would exceed
|
|
236
|
+
:data:`CLIENT_ORDER_ID_MAX_LEN` (indicates ``retry_seq`` overflow).
|
|
237
|
+
"""
|
|
238
|
+
if len(run_tag) != RUN_TAG_WIDTH or not run_tag.isascii() or not run_tag.isalnum():
|
|
239
|
+
raise ValueError(
|
|
240
|
+
f"run_tag must be {RUN_TAG_WIDTH} alphanumeric ASCII chars, "
|
|
241
|
+
f"got {run_tag!r}",
|
|
242
|
+
)
|
|
243
|
+
if kind not in VALID_KINDS:
|
|
244
|
+
raise ValueError(
|
|
245
|
+
f"kind must be one of {sorted(VALID_KINDS)}, got {kind!r}",
|
|
246
|
+
)
|
|
247
|
+
if bar_ts_ms < 0:
|
|
248
|
+
raise ValueError(f"bar_ts_ms must be non-negative, got {bar_ts_ms}")
|
|
249
|
+
if retry_seq < 0:
|
|
250
|
+
raise ValueError(f"retry_seq must be non-negative, got {retry_seq}")
|
|
251
|
+
|
|
252
|
+
pid = hash_pine_id(pine_id)
|
|
253
|
+
bar = _to_base36(bar_ts_ms, width=BAR_TS_WIDTH)
|
|
254
|
+
retry = _to_base36(retry_seq)
|
|
255
|
+
|
|
256
|
+
result = f"{run_tag}-{pid}-{bar}-{kind}{retry}"
|
|
257
|
+
if len(result) > CLIENT_ORDER_ID_MAX_LEN:
|
|
258
|
+
raise ValueError(
|
|
259
|
+
f"client_order_id exceeds {CLIENT_ORDER_ID_MAX_LEN} chars "
|
|
260
|
+
f"(got {len(result)}); retry_seq={retry_seq} overflows the budget",
|
|
261
|
+
)
|
|
262
|
+
return result
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def encode_wire_client_order_id(coid: str, max_len: int) -> str:
|
|
266
|
+
"""Encode a canonical client-order-id for a venue's client-id budget.
|
|
267
|
+
|
|
268
|
+
Identity when the canonical id fits (``len(coid) <= max_len``) — venues
|
|
269
|
+
accepting the full canonical width are unaffected. Otherwise returns the
|
|
270
|
+
fixed-length wire form ``{run4}{bar9}{kind}{hash}`` of exactly ``max_len``
|
|
271
|
+
characters (module docstring, "Wire form for short-budget venues"). Pure
|
|
272
|
+
and deterministic like :func:`build_client_order_id`, so retries,
|
|
273
|
+
restarts and journal-rebuilt references converge on the same wire id.
|
|
274
|
+
|
|
275
|
+
:param coid: A canonical id from :func:`build_client_order_id`.
|
|
276
|
+
:param max_len: The venue's client-id budget (the plugin's
|
|
277
|
+
``client_order_id_max_len``). Must be at least
|
|
278
|
+
:data:`WIRE_CLIENT_ORDER_ID_MIN_LEN` when shortening is needed.
|
|
279
|
+
:raises ValueError: When ``coid`` is not a well-formed canonical id, or
|
|
280
|
+
the budget is below the wire floor.
|
|
281
|
+
"""
|
|
282
|
+
if len(coid) <= max_len:
|
|
283
|
+
return coid
|
|
284
|
+
if max_len < WIRE_CLIENT_ORDER_ID_MIN_LEN:
|
|
285
|
+
raise ValueError(
|
|
286
|
+
f"client-id budget {max_len} is below the wire floor "
|
|
287
|
+
f"{WIRE_CLIENT_ORDER_ID_MIN_LEN}",
|
|
288
|
+
)
|
|
289
|
+
parsed = parse_client_order_id(coid)
|
|
290
|
+
if parsed is None:
|
|
291
|
+
raise ValueError(f"not a canonical client_order_id: {coid!r}")
|
|
292
|
+
hash_width = max_len - WIRE_RAW_PREFIX_LEN
|
|
293
|
+
digest = hashlib.sha256(coid.encode('utf-8')).digest()
|
|
294
|
+
tail_value = int.from_bytes(digest, 'big') % (36 ** hash_width)
|
|
295
|
+
tail = _to_base36(tail_value, width=hash_width)
|
|
296
|
+
bar = _to_base36(parsed.bar_ts_ms, width=BAR_TS_WIDTH)
|
|
297
|
+
return f"{parsed.run_tag}{bar}{parsed.kind}{tail}"
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
301
|
+
class ParsedClientOrderId:
|
|
302
|
+
"""Structural decomposition of a canonical client-order-id.
|
|
303
|
+
|
|
304
|
+
The ``pine_id`` is irrecoverable — :func:`hash_pine_id` is one-way — so
|
|
305
|
+
:attr:`pid_hash` carries the 8-char hash instead. A caller that knows a
|
|
306
|
+
candidate ``pine_id`` matches by forward-hashing it
|
|
307
|
+
(``hash_pine_id(candidate) == parsed.pid_hash``).
|
|
308
|
+
"""
|
|
309
|
+
run_tag: str
|
|
310
|
+
pid_hash: str
|
|
311
|
+
bar_ts_ms: int
|
|
312
|
+
kind: str
|
|
313
|
+
retry_seq: int
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def parse_client_order_id(coid: str) -> ParsedClientOrderId | None:
|
|
317
|
+
"""Parse a canonical client-order-id back into its structural fields.
|
|
318
|
+
|
|
319
|
+
The inverse of :func:`build_client_order_id`, modulo the one-way
|
|
320
|
+
``pine_id`` hash (see :class:`ParsedClientOrderId`). Used by the sync
|
|
321
|
+
engine's restart adoption path to recognise the bot's own live broker
|
|
322
|
+
orders from their echoed ``client_order_id`` and recover the
|
|
323
|
+
``(bar_ts_ms, retry_seq)`` anchor a crash dropped before it was
|
|
324
|
+
journaled.
|
|
325
|
+
|
|
326
|
+
Best-effort and total: any input that does not match the
|
|
327
|
+
``{run4}-{pid8}-{bar9}-{kind}{retry}`` shape — wrong dash count, wrong
|
|
328
|
+
field widths, an unknown ``kind`` code, or a non-base36 ``bar`` /
|
|
329
|
+
``retry`` — yields ``None`` rather than raising, so a caller can pass an
|
|
330
|
+
externally-owned order's id straight through.
|
|
331
|
+
|
|
332
|
+
:param coid: The client-order-id to parse.
|
|
333
|
+
:return: The decomposed fields, or ``None`` when ``coid`` is not a
|
|
334
|
+
well-formed canonical id.
|
|
335
|
+
"""
|
|
336
|
+
parts = coid.split('-')
|
|
337
|
+
if len(parts) != 4:
|
|
338
|
+
return None
|
|
339
|
+
run_tag, pid_hash, bar_b36, kind_retry = parts
|
|
340
|
+
if (len(run_tag) != RUN_TAG_WIDTH
|
|
341
|
+
or len(pid_hash) != PINE_ID_HASH_WIDTH
|
|
342
|
+
or len(bar_b36) != BAR_TS_WIDTH
|
|
343
|
+
or len(kind_retry) < 2):
|
|
344
|
+
return None
|
|
345
|
+
kind = kind_retry[0]
|
|
346
|
+
if kind not in VALID_KINDS:
|
|
347
|
+
return None
|
|
348
|
+
retry_b36 = kind_retry[1:]
|
|
349
|
+
base36 = set(_BASE36_DIGITS)
|
|
350
|
+
if not (set(bar_b36) <= base36 and set(retry_b36) <= base36):
|
|
351
|
+
return None
|
|
352
|
+
return ParsedClientOrderId(
|
|
353
|
+
run_tag=run_tag,
|
|
354
|
+
pid_hash=pid_hash,
|
|
355
|
+
bar_ts_ms=int(bar_b36, 36),
|
|
356
|
+
kind=kind,
|
|
357
|
+
retry_seq=int(retry_b36, 36),
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
362
|
+
class ParsedWireClientOrderId:
|
|
363
|
+
"""Raw-prefix fields of a wire-form client-order-id.
|
|
364
|
+
|
|
365
|
+
Only the fields carried verbatim in the wire prefix are recoverable —
|
|
366
|
+
the ``pid`` hash and ``retry_seq`` live inside the opaque sha256 tail.
|
|
367
|
+
A caller that knows a candidate ``(pine_id, retry_seq)`` matches by
|
|
368
|
+
rebuilding the canonical id and re-encoding it at the echoed id's length
|
|
369
|
+
(``encode_wire_client_order_id(candidate, len(coid)) == coid``).
|
|
370
|
+
"""
|
|
371
|
+
run_tag: str
|
|
372
|
+
bar_ts_ms: int
|
|
373
|
+
kind: str
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def parse_wire_client_order_id(coid: str) -> ParsedWireClientOrderId | None:
|
|
377
|
+
"""Parse the raw prefix of a wire-form client-order-id.
|
|
378
|
+
|
|
379
|
+
Best-effort and total like :func:`parse_client_order_id`: anything that
|
|
380
|
+
does not match the ``{run4}{bar9}{kind}{hash>=6}`` all-base36 shape —
|
|
381
|
+
too short, containing a dash (canonical ids always do), an unknown
|
|
382
|
+
``kind`` code — yields ``None``. Budget-independent: the wire form's
|
|
383
|
+
length always equals the minting venue's budget, so the parser only
|
|
384
|
+
enforces the :data:`WIRE_CLIENT_ORDER_ID_MIN_LEN` floor.
|
|
385
|
+
|
|
386
|
+
:param coid: The client-order-id to parse.
|
|
387
|
+
:return: The raw-prefix fields, or ``None`` when ``coid`` is not a
|
|
388
|
+
well-formed wire id.
|
|
389
|
+
"""
|
|
390
|
+
if len(coid) < WIRE_CLIENT_ORDER_ID_MIN_LEN:
|
|
391
|
+
return None
|
|
392
|
+
if not (set(coid) <= set(_BASE36_DIGITS)):
|
|
393
|
+
return None
|
|
394
|
+
kind = coid[WIRE_RAW_PREFIX_LEN - 1]
|
|
395
|
+
if kind not in VALID_KINDS:
|
|
396
|
+
return None
|
|
397
|
+
return ParsedWireClientOrderId(
|
|
398
|
+
run_tag=coid[:RUN_TAG_WIDTH],
|
|
399
|
+
bar_ts_ms=int(coid[RUN_TAG_WIDTH:RUN_TAG_WIDTH + BAR_TS_WIDTH], 36),
|
|
400
|
+
kind=kind,
|
|
401
|
+
)
|