opencode-pyneruntime 6.6.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. opencode_pyneruntime-6.6.4.dist-info/METADATA +281 -0
  2. opencode_pyneruntime-6.6.4.dist-info/RECORD +261 -0
  3. opencode_pyneruntime-6.6.4.dist-info/WHEEL +5 -0
  4. opencode_pyneruntime-6.6.4.dist-info/entry_points.txt +6 -0
  5. opencode_pyneruntime-6.6.4.dist-info/licenses/LICENSE +201 -0
  6. opencode_pyneruntime-6.6.4.dist-info/licenses/NOTICE +21 -0
  7. opencode_pyneruntime-6.6.4.dist-info/top_level.txt +1 -0
  8. pynecore/__init__.py +6 -0
  9. pynecore/cli/__init__.py +2 -0
  10. pynecore/cli/app.py +238 -0
  11. pynecore/cli/commands/__init__.py +343 -0
  12. pynecore/cli/commands/benchmark.py +186 -0
  13. pynecore/cli/commands/compile.py +198 -0
  14. pynecore/cli/commands/data.py +857 -0
  15. pynecore/cli/commands/debug.py +63 -0
  16. pynecore/cli/commands/optimize.py +956 -0
  17. pynecore/cli/commands/plugin.py +242 -0
  18. pynecore/cli/commands/run.py +2006 -0
  19. pynecore/cli/pluggable.py +132 -0
  20. pynecore/cli/utils/__init__.py +0 -0
  21. pynecore/cli/utils/api_error_handler.py +168 -0
  22. pynecore/cli/utils/broker_picker.py +330 -0
  23. pynecore/cli/utils/error_hook.py +28 -0
  24. pynecore/cli/utils/keyreader.py +178 -0
  25. pynecore/cli/utils/provider_picker.py +19 -0
  26. pynecore/cli/utils/symbol_browser.py +1149 -0
  27. pynecore/core/__init__.py +0 -0
  28. pynecore/core/aggregator.py +257 -0
  29. pynecore/core/bar_magnifier.py +168 -0
  30. pynecore/core/broker/__init__.py +64 -0
  31. pynecore/core/broker/defaults.py +113 -0
  32. pynecore/core/broker/disappearance.py +927 -0
  33. pynecore/core/broker/emulator.py +345 -0
  34. pynecore/core/broker/exceptions.py +346 -0
  35. pynecore/core/broker/idempotency.py +401 -0
  36. pynecore/core/broker/intent_builder.py +334 -0
  37. pynecore/core/broker/journal.py +1785 -0
  38. pynecore/core/broker/models.py +1600 -0
  39. pynecore/core/broker/native_failsafe_manager.py +1436 -0
  40. pynecore/core/broker/one_way_emulator.py +1128 -0
  41. pynecore/core/broker/position.py +787 -0
  42. pynecore/core/broker/run_identity.py +126 -0
  43. pynecore/core/broker/software_entry_stop_engine.py +351 -0
  44. pynecore/core/broker/software_partial_bracket_engine.py +1379 -0
  45. pynecore/core/broker/spot_inventory.py +1327 -0
  46. pynecore/core/broker/storage.py +2655 -0
  47. pynecore/core/broker/store_helpers.py +2161 -0
  48. pynecore/core/broker/sync_engine.py +16070 -0
  49. pynecore/core/broker/validation.py +382 -0
  50. pynecore/core/class_property.py +7 -0
  51. pynecore/core/config.py +392 -0
  52. pynecore/core/csv_file.py +547 -0
  53. pynecore/core/currency.py +262 -0
  54. pynecore/core/data_converter.py +1002 -0
  55. pynecore/core/datetime.py +296 -0
  56. pynecore/core/download_info.py +71 -0
  57. pynecore/core/download_runner.py +274 -0
  58. pynecore/core/htf_aggregator.py +181 -0
  59. pynecore/core/import_hook.py +358 -0
  60. pynecore/core/instance_state.py +494 -0
  61. pynecore/core/live_ltf_collector.py +442 -0
  62. pynecore/core/live_ltf_window.py +189 -0
  63. pynecore/core/live_runner.py +1347 -0
  64. pynecore/core/module_property.py +26 -0
  65. pynecore/core/ohlcv_file.py +1888 -0
  66. pynecore/core/overload.py +371 -0
  67. pynecore/core/pine_cast.py +113 -0
  68. pynecore/core/pine_export.py +95 -0
  69. pynecore/core/pine_method.py +244 -0
  70. pynecore/core/pine_range.py +86 -0
  71. pynecore/core/pine_udt.py +69 -0
  72. pynecore/core/plugin/__init__.py +394 -0
  73. pynecore/core/plugin/broker.py +781 -0
  74. pynecore/core/plugin/cli.py +96 -0
  75. pynecore/core/plugin/live_provider.py +208 -0
  76. pynecore/core/plugin/provider.py +331 -0
  77. pynecore/core/provider_string.py +148 -0
  78. pynecore/core/random.py +40 -0
  79. pynecore/core/resampler.py +686 -0
  80. pynecore/core/safe_convert.py +64 -0
  81. pynecore/core/script.py +1011 -0
  82. pynecore/core/script_runner.py +3202 -0
  83. pynecore/core/security.py +1749 -0
  84. pynecore/core/security_process.py +1253 -0
  85. pynecore/core/security_shm.py +456 -0
  86. pynecore/core/series.py +417 -0
  87. pynecore/core/strategy_stats.py +669 -0
  88. pynecore/core/symbol_map.py +134 -0
  89. pynecore/core/syminfo.py +505 -0
  90. pynecore/core/viz.py +591 -0
  91. pynecore/lib/__init__.py +1771 -0
  92. pynecore/lib/_fixnan.py +32 -0
  93. pynecore/lib/_math_stateful.py +202 -0
  94. pynecore/lib/_timeframe_change.py +101 -0
  95. pynecore/lib/adjustment.py +6 -0
  96. pynecore/lib/alert.py +39 -0
  97. pynecore/lib/alert.pyi +14 -0
  98. pynecore/lib/array.py +1051 -0
  99. pynecore/lib/barmerge.py +60 -0
  100. pynecore/lib/barstate.py +30 -0
  101. pynecore/lib/box.py +415 -0
  102. pynecore/lib/chart.py +128 -0
  103. pynecore/lib/color.py +152 -0
  104. pynecore/lib/color.pyi +50 -0
  105. pynecore/lib/currency.py +62 -0
  106. pynecore/lib/dayofweek.py +36 -0
  107. pynecore/lib/dayofweek.pyi +18 -0
  108. pynecore/lib/display.py +8 -0
  109. pynecore/lib/dividends.py +9 -0
  110. pynecore/lib/earnings.py +11 -0
  111. pynecore/lib/extend.py +6 -0
  112. pynecore/lib/font.py +5 -0
  113. pynecore/lib/footprint.py +79 -0
  114. pynecore/lib/format.py +11 -0
  115. pynecore/lib/hline.py +67 -0
  116. pynecore/lib/hline.pyi +24 -0
  117. pynecore/lib/label.py +409 -0
  118. pynecore/lib/line.py +433 -0
  119. pynecore/lib/linefill.py +93 -0
  120. pynecore/lib/location.py +11 -0
  121. pynecore/lib/log.py +362 -0
  122. pynecore/lib/map.py +150 -0
  123. pynecore/lib/math.py +385 -0
  124. pynecore/lib/matrix.py +708 -0
  125. pynecore/lib/order.py +8 -0
  126. pynecore/lib/pivotpointtype.py +8 -0
  127. pynecore/lib/plot.py +95 -0
  128. pynecore/lib/plot.pyi +33 -0
  129. pynecore/lib/polyline.py +91 -0
  130. pynecore/lib/position.py +15 -0
  131. pynecore/lib/request.py +281 -0
  132. pynecore/lib/runtime.py +5 -0
  133. pynecore/lib/scale.py +9 -0
  134. pynecore/lib/session.py +267 -0
  135. pynecore/lib/session.pyi +12 -0
  136. pynecore/lib/shape.py +18 -0
  137. pynecore/lib/size.py +12 -0
  138. pynecore/lib/splits.py +4 -0
  139. pynecore/lib/strategy/__init__.py +4778 -0
  140. pynecore/lib/strategy/closedtrades.py +347 -0
  141. pynecore/lib/strategy/closedtrades.pyi +53 -0
  142. pynecore/lib/strategy/commission.py +9 -0
  143. pynecore/lib/strategy/direction.py +9 -0
  144. pynecore/lib/strategy/oca.py +13 -0
  145. pynecore/lib/strategy/opentrades.py +281 -0
  146. pynecore/lib/strategy/opentrades.pyi +49 -0
  147. pynecore/lib/strategy/risk.py +109 -0
  148. pynecore/lib/string.py +649 -0
  149. pynecore/lib/syminfo.py +84 -0
  150. pynecore/lib/ta.py +2230 -0
  151. pynecore/lib/table.py +290 -0
  152. pynecore/lib/text.py +17 -0
  153. pynecore/lib/ticker.py +207 -0
  154. pynecore/lib/timeframe.py +293 -0
  155. pynecore/lib/volume_row.py +67 -0
  156. pynecore/lib/xloc.py +4 -0
  157. pynecore/lib/yloc.py +5 -0
  158. pynecore/providers/__init__.py +0 -0
  159. pynecore/providers/ccxt.py +664 -0
  160. pynecore/providers/replay.py +187 -0
  161. pynecore/pynesys/__init__.py +0 -0
  162. pynecore/pynesys/api.py +498 -0
  163. pynecore/pynesys/compiler.py +112 -0
  164. pynecore/standalone.py +99 -0
  165. pynecore/testing/__init__.py +1 -0
  166. pynecore/testing/broker_lab/__init__.py +41 -0
  167. pynecore/testing/broker_lab/__main__.py +5 -0
  168. pynecore/testing/broker_lab/cli.py +87 -0
  169. pynecore/testing/broker_lab/generate.py +47 -0
  170. pynecore/testing/broker_lab/model.py +84 -0
  171. pynecore/testing/broker_lab/reference.py +645 -0
  172. pynecore/testing/broker_lab/runner.py +372 -0
  173. pynecore/testing/broker_lab/scheduler.py +50 -0
  174. pynecore/testing/broker_lab/subprocess.py +73 -0
  175. pynecore/transformers/__init__.py +0 -0
  176. pynecore/transformers/builtin_shadow.py +136 -0
  177. pynecore/transformers/closure_arguments_transformer.py +428 -0
  178. pynecore/transformers/display_rewrite.py +140 -0
  179. pynecore/transformers/dynamic_default.py +147 -0
  180. pynecore/transformers/function_isolation.py +757 -0
  181. pynecore/transformers/import_lifter.py +61 -0
  182. pynecore/transformers/import_normalizer.py +328 -0
  183. pynecore/transformers/inline_series_hoist.py +178 -0
  184. pynecore/transformers/input_transformer.py +175 -0
  185. pynecore/transformers/lib_series.py +201 -0
  186. pynecore/transformers/locations.py +70 -0
  187. pynecore/transformers/module_properties.json +3387 -0
  188. pynecore/transformers/module_property.py +221 -0
  189. pynecore/transformers/ne_guard.py +70 -0
  190. pynecore/transformers/persistent.py +320 -0
  191. pynecore/transformers/persistent_series.py +76 -0
  192. pynecore/transformers/safe_convert_transformer.py +97 -0
  193. pynecore/transformers/safe_division_transformer.py +95 -0
  194. pynecore/transformers/script_requirements.py +308 -0
  195. pynecore/transformers/security.py +752 -0
  196. pynecore/transformers/security_instantiation.py +274 -0
  197. pynecore/transformers/series.py +275 -0
  198. pynecore/transformers/slot_layout.py +381 -0
  199. pynecore/transformers/type_checking_stripper.py +25 -0
  200. pynecore/transformers/unused_series_detector.py +267 -0
  201. pynecore/types/__init__.py +21 -0
  202. pynecore/types/alert.py +5 -0
  203. pynecore/types/barmerge.py +5 -0
  204. pynecore/types/base.py +39 -0
  205. pynecore/types/box.py +37 -0
  206. pynecore/types/chart.py +17 -0
  207. pynecore/types/color.py +107 -0
  208. pynecore/types/currency.py +5 -0
  209. pynecore/types/datetime.py +6 -0
  210. pynecore/types/display.py +5 -0
  211. pynecore/types/dividends.py +5 -0
  212. pynecore/types/earnings.py +5 -0
  213. pynecore/types/extend.py +5 -0
  214. pynecore/types/font.py +5 -0
  215. pynecore/types/footprint.py +41 -0
  216. pynecore/types/format.py +5 -0
  217. pynecore/types/hline.py +24 -0
  218. pynecore/types/ib_persistent.py +8 -0
  219. pynecore/types/ib_persistent.pyi +10 -0
  220. pynecore/types/label.py +35 -0
  221. pynecore/types/line.py +32 -0
  222. pynecore/types/linefill.py +13 -0
  223. pynecore/types/location.py +5 -0
  224. pynecore/types/matrix.py +999 -0
  225. pynecore/types/na.py +237 -0
  226. pynecore/types/na.pyi +83 -0
  227. pynecore/types/ohlcv.py +12 -0
  228. pynecore/types/order.py +5 -0
  229. pynecore/types/persistent.py +8 -0
  230. pynecore/types/persistent.pyi +13 -0
  231. pynecore/types/pine_types.py +11 -0
  232. pynecore/types/pine_types.pyi +15 -0
  233. pynecore/types/pivotpointtype.py +5 -0
  234. pynecore/types/plot.py +12 -0
  235. pynecore/types/plot_meta.py +60 -0
  236. pynecore/types/polyline.py +40 -0
  237. pynecore/types/position.py +5 -0
  238. pynecore/types/scale.py +5 -0
  239. pynecore/types/script_type.py +15 -0
  240. pynecore/types/series.py +23 -0
  241. pynecore/types/series.pyi +19 -0
  242. pynecore/types/session.py +35 -0
  243. pynecore/types/shape.py +5 -0
  244. pynecore/types/size.py +5 -0
  245. pynecore/types/source.py +33 -0
  246. pynecore/types/splits.py +5 -0
  247. pynecore/types/strategy.py +45 -0
  248. pynecore/types/table.py +87 -0
  249. pynecore/types/text.py +13 -0
  250. pynecore/types/type_checker.py +7 -0
  251. pynecore/types/type_checker.pyi +48 -0
  252. pynecore/types/volume_row.py +36 -0
  253. pynecore/types/weekdays.py +11 -0
  254. pynecore/types/xloc.py +5 -0
  255. pynecore/types/yloc.py +5 -0
  256. pynecore/utils/__init__.py +0 -0
  257. pynecore/utils/file_utils.py +50 -0
  258. pynecore/utils/rich/__init__.py +0 -0
  259. pynecore/utils/rich/date_column.py +25 -0
  260. pynecore/utils/sequence_view.py +92 -0
  261. pynecore/utils/stdlib_checker.py +17 -0
@@ -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
+ )