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,382 @@
1
+ """
2
+ Startup-time validation for broker mode.
3
+
4
+ - :func:`validate_at_startup` — script :class:`ScriptRequirements` against a
5
+ plugin's :class:`ExchangeCapabilities`.
6
+ - :func:`validate_plugin_contract` — the plugin itself against the
7
+ :class:`~pynecore.core.plugin.broker.BrokerPlugin` authoring contract
8
+ (override pairs, capability-declaration consistency, lifecycle state).
9
+
10
+ Pure functions — the ``pyne run --broker`` startup path calls them and, on a
11
+ non-empty error list, refuses to start trading.
12
+ """
13
+ import math
14
+ from dataclasses import fields
15
+
16
+ from pynecore.core.broker.idempotency import WIRE_CLIENT_ORDER_ID_MIN_LEN
17
+ from pynecore.core.broker.models import (
18
+ CapabilityLevel,
19
+ ExchangeCapabilities,
20
+ ScriptRequirements,
21
+ )
22
+ from pynecore.core.plugin.broker import BrokerPlugin
23
+
24
+ __all__ = ['validate_at_startup', 'validate_plugin_contract']
25
+
26
+ #: Methods a :class:`~pynecore.core.plugin.broker.PositionPort` implementation
27
+ #: must provide. Mirrors the Protocol surface — kept here as data so the
28
+ #: contract probe can enumerate it without runtime Protocol introspection.
29
+ _POSITION_PORT_METHODS = (
30
+ 'fetch_raw_positions',
31
+ 'get_volume_quantizer',
32
+ 'close_leg',
33
+ 'reject_out_of_range',
34
+ 'place_leg',
35
+ 'amend_bracket',
36
+ )
37
+
38
+ #: Surface a :class:`~pynecore.core.broker.spot_inventory.SpotInventoryPort`
39
+ #: implementation must provide — same enumerate-as-data approach as above.
40
+ _SPOT_INVENTORY_PORT_METHODS = (
41
+ 'fetch_executions',
42
+ 'fetch_base_balance',
43
+ )
44
+ _SPOT_INVENTORY_PORT_ATTRS = (
45
+ 'product_id',
46
+ 'base_asset',
47
+ 'quote_asset',
48
+ 'cursor_scope',
49
+ 'base_tolerance',
50
+ 'settlement_grace_s',
51
+ 'position_dust_threshold',
52
+ )
53
+
54
+
55
+ def validate_at_startup(
56
+ reqs: ScriptRequirements,
57
+ caps: ExchangeCapabilities,
58
+ pyramiding: int = 1,
59
+ ) -> list[str]:
60
+ """
61
+ Return a list of human-readable error strings — empty if all requirements
62
+ are satisfied by the exchange capabilities.
63
+
64
+ The rule is simple: if the script uses a Pine parameter, the exchange
65
+ must support the corresponding capability at *any* level (SOFTWARE,
66
+ PARTIAL_NATIVE or NATIVE). Only :data:`~pynecore.core.broker.models.
67
+ CapabilityLevel.UNSUPPORTED` fails — the level distinction is a
68
+ diagnostic, not a stricter contract, because :class:`ScriptRequirements`
69
+ has no channel today to declare a "must be native" requirement.
70
+ Safety-first: better to refuse to start than to fail on the first
71
+ unexpected bar in live trading.
72
+
73
+ :param reqs: The running script's :class:`ScriptRequirements`.
74
+ :param caps: The plugin's advertised :class:`ExchangeCapabilities`.
75
+ :param pyramiding: ``strategy(pyramiding=...)`` from the running script;
76
+ ``1`` (the default) means single-row, where the partial-qty bracket
77
+ path is always safe — *unless* the script also calls
78
+ ``strategy.order()``, which is exempt from the pyramiding cap and
79
+ can open multiple same-id rows on its own. ``pyramiding > 1`` or
80
+ ``reqs.strategy_order=True`` activates the
81
+ :attr:`ExchangeCapabilities.partial_qty_bracket_exit_pyramiding`
82
+ gate: the intent builder's
83
+ ``entry_orders[from_entry]`` lookup keys on a single Pine entry id
84
+ and would silently use the latest row's quantity if multiple rows
85
+ share that id, so the validator refuses to start until the plugin
86
+ explicitly opts the multi-row path in.
87
+ """
88
+ errors: list[str] = []
89
+ if reqs.stop_orders and not caps.stop_order.is_supported:
90
+ errors.append(
91
+ "Script uses stop orders, but the exchange doesn't support them."
92
+ )
93
+ if reqs.tp_sl_bracket and not caps.tp_sl_bracket.is_supported:
94
+ errors.append(
95
+ "Script uses TP+SL exit brackets (OCA reduce), but the exchange "
96
+ "plugin doesn't support them. Use a plugin that emulates this, "
97
+ "or modify the script."
98
+ )
99
+ if reqs.trailing_stop and not caps.trailing_stop.is_supported:
100
+ errors.append(
101
+ "Script uses trailing stops, but the exchange doesn't support them."
102
+ )
103
+ if reqs.exit_orders and not caps.reduce_only.is_supported:
104
+ errors.append(
105
+ "Script uses strategy.exit / strategy.close, but the exchange "
106
+ "doesn't support reduce-only orders. A later-arriving exit "
107
+ "could flip the book to the other side once the position is "
108
+ "already closed — refuse to start."
109
+ )
110
+ if reqs.partial_qty_bracket_exit and not caps.partial_qty_bracket_exit.is_supported:
111
+ errors.append(
112
+ "Script calls strategy.exit(qty=N, from_entry='L', ...) with a "
113
+ "bracket parameter (limit/stop/profit/loss/trail_*) where N is "
114
+ "less than the total qty entered under 'L', but the exchange "
115
+ "only supports full-row position-attribute brackets. The plugin "
116
+ "cannot attach TP/SL to a partial quantity, and silently "
117
+ "covering the full row would mis-hedge the strategy. Either "
118
+ "split into (a) strategy.exit(qty=N) without bracket + "
119
+ "(b) strategy.exit with bracket on the full row, or use a "
120
+ "different broker."
121
+ )
122
+ if (
123
+ reqs.partial_qty_bracket_exit
124
+ and caps.partial_qty_bracket_exit.is_supported
125
+ and (pyramiding > 1 or reqs.strategy_order)
126
+ and not caps.partial_qty_bracket_exit_pyramiding.is_supported
127
+ ):
128
+ if reqs.strategy_order and pyramiding <= 1:
129
+ trigger = (
130
+ "Script uses strategy.order() (which is exempt from the "
131
+ "pyramiding cap and can open multiple same-id rows) "
132
+ )
133
+ else:
134
+ trigger = "Script combines strategy(pyramiding>1) "
135
+ errors.append(
136
+ trigger +
137
+ "with a partial-qty exit bracket "
138
+ "(strategy.exit(qty=N, from_entry='L', ...) where N is "
139
+ "less than the row total). This exchange plugin's partial-qty "
140
+ "bracket path is single-row only — multiple parent entries "
141
+ "sharing one Pine entry id would be routed against just the "
142
+ "latest row's declared quantity, silently mis-hedging the "
143
+ "older rows. Use strategy(pyramiding=1) without "
144
+ "strategy.order() on this broker, or switch to a plugin that "
145
+ "opts into partial-qty bracket pyramiding support."
146
+ )
147
+ if reqs.may_go_short and not caps.short_selling.is_supported:
148
+ errors.append(
149
+ "Script passes a constant strategy.short direction to "
150
+ "strategy.entry / strategy.order, but the exchange doesn't "
151
+ "support short selling (spot venue — a negative base position "
152
+ "cannot exist). Remove the short side, or trade on a "
153
+ "margin-capable broker."
154
+ )
155
+ return errors
156
+
157
+
158
+ def validate_plugin_contract(
159
+ plugin: BrokerPlugin,
160
+ *,
161
+ require_account_id: bool = False,
162
+ ) -> tuple[list[str], list[str]]:
163
+ """
164
+ Probe a broker plugin against the enforceable parts of the
165
+ :class:`~pynecore.core.plugin.broker.BrokerPlugin` authoring contract.
166
+
167
+ The abstract ``execute_*`` surface is small, but the real contract lives
168
+ in docstring prose that a new plugin author can silently miss. This probe
169
+ turns the machine-checkable subset into fail-fast startup errors:
170
+
171
+ - **Override pairs** — a plugin that overrides
172
+ :meth:`~pynecore.core.plugin.broker.BrokerPlugin.get_residual_orders_after_bracket_attach_reject`
173
+ returns broker refs the engine hands back to
174
+ :meth:`~pynecore.core.plugin.broker.BrokerPlugin.cancel_broker_order_ref`,
175
+ whose default raises :class:`NotImplementedError` — the defensive-close
176
+ recovery loop would crash exactly when it is needed.
177
+ - **Capability declaration consistency** — every
178
+ :class:`ExchangeCapabilities` field must be a :class:`CapabilityLevel`
179
+ (a ``True``/``False`` slips through type checkers on untyped call
180
+ sites); a supported ``watch_orders`` needs the method actually
181
+ overridden; a NATIVE / PARTIAL_NATIVE ``amend_order`` claim needs at
182
+ least one of ``modify_entry`` / ``modify_exit`` overridden (the
183
+ inherited defaults are cancel+recreate, which the declaration denies).
184
+ - **Idempotency floor** — ``idempotency=UNSUPPORTED`` means restart /
185
+ timeout retries can double-fill; live trading is refused
186
+ (:class:`CapabilityLevel` documents this rejection, this is where it
187
+ is enforced).
188
+ - **Client-id budget** — ``client_order_id_max_len`` must be an int of
189
+ at least :data:`~pynecore.core.broker.idempotency.WIRE_CLIENT_ORDER_ID_MIN_LEN`;
190
+ the wire-form client-order-id cannot stay deterministic-and-recognisable
191
+ below that.
192
+ - **Lifecycle** — with ``require_account_id=True`` the plugin must have
193
+ populated ``_account_id`` during authentication *before* the broker
194
+ storage derives the run identity from it; a silent ``"default"``
195
+ would collide every run of the account.
196
+ - **PositionPort surface** — a non-``None``
197
+ :attr:`~pynecore.core.plugin.broker.BrokerPlugin.position_port` must
198
+ carry the full port surface the core
199
+ :class:`~pynecore.core.broker.one_way_emulator.OneWayEmulator` drives.
200
+
201
+ Deliberately NOT checked: ``cancel_all`` capability vs
202
+ ``execute_cancel_all`` override. A ``SOFTWARE`` ``cancel_all`` is
203
+ legitimately delivered through the sync engine's diff loop as per-intent
204
+ cancels with the default ``execute_cancel_all`` untouched (Capital.com
205
+ does exactly this).
206
+
207
+ Warnings flag legal but degraded setups the author should confirm are
208
+ intentional; they must not block startup.
209
+
210
+ :param plugin: The instantiated broker plugin to probe.
211
+ :param require_account_id: ``True`` on the production ``--broker`` path,
212
+ where authentication has already run and the broker storage is about
213
+ to derive the run identity from :attr:`BrokerPlugin.account_id`.
214
+ Leave ``False`` for paths that never open broker storage.
215
+ :return: ``(errors, warnings)`` — human-readable strings; empty lists
216
+ when the plugin conforms.
217
+ """
218
+ errors: list[str] = []
219
+ warnings: list[str] = []
220
+ cls = type(plugin)
221
+ name = cls.__name__
222
+
223
+ def overridden(method: str) -> bool:
224
+ return getattr(cls, method) is not getattr(BrokerPlugin, method)
225
+
226
+ # --- Capability declaration ---
227
+ caps = plugin.get_capabilities()
228
+ bad_fields: set[str] = set()
229
+ for f in fields(ExchangeCapabilities):
230
+ value = getattr(caps, f.name)
231
+ if not isinstance(value, CapabilityLevel):
232
+ bad_fields.add(f.name)
233
+ errors.append(
234
+ f"{name}.get_capabilities().{f.name} is {value!r} "
235
+ f"({type(value).__name__}) — every capability field must be "
236
+ f"a CapabilityLevel, never a bool or plain string."
237
+ )
238
+
239
+ if 'idempotency' not in bad_fields and not caps.idempotency.is_supported:
240
+ errors.append(
241
+ f"{name} declares idempotency=UNSUPPORTED — without client-id "
242
+ f"echo or dedup, restart/timeout retries can double-fill. Live "
243
+ f"trading is refused; declare SOFTWARE and dedup locally (see "
244
+ f"the Capital.com plugin) if the exchange offers nothing."
245
+ )
246
+
247
+ if 'watch_orders' not in bad_fields:
248
+ if caps.watch_orders.is_supported and not overridden('watch_orders'):
249
+ errors.append(
250
+ f"{name} declares watch_orders={caps.watch_orders.name} but "
251
+ f"does not override watch_orders() — the base method raises "
252
+ f"NotImplementedError, so the declared order stream cannot "
253
+ f"exist. Either implement the stream or declare UNSUPPORTED."
254
+ )
255
+ elif not overridden('watch_orders'):
256
+ warnings.append(
257
+ f"{name} has no watch_orders() stream: the engine falls back "
258
+ f"to reconcile() polling for fills, and there is NO channel "
259
+ f"for bot-owned-order disappearance detection (manual closes, "
260
+ f"broker liquidations and silent cancels stay invisible). "
261
+ f"Confirm this is acceptable for the venue."
262
+ )
263
+
264
+ if ('amend_order' not in bad_fields
265
+ and caps.amend_order in (CapabilityLevel.NATIVE, CapabilityLevel.PARTIAL_NATIVE)
266
+ and not overridden('modify_entry')
267
+ and not overridden('modify_exit')):
268
+ errors.append(
269
+ f"{name} declares amend_order={caps.amend_order.name} but "
270
+ f"overrides neither modify_entry() nor modify_exit() — the "
271
+ f"inherited defaults are cancel+recreate (an unprotected window "
272
+ f"the declaration claims not to have). Override at least one "
273
+ f"with the exchange's in-place amend, or declare SOFTWARE."
274
+ )
275
+
276
+ # --- Client-id budget ---
277
+ max_len = plugin.client_order_id_max_len
278
+ if not isinstance(max_len, int) or isinstance(max_len, bool):
279
+ errors.append(
280
+ f"{name}.client_order_id_max_len is {max_len!r} "
281
+ f"({type(max_len).__name__}) — must be an int (the venue's "
282
+ f"client-order-id length limit in characters)."
283
+ )
284
+ elif max_len < WIRE_CLIENT_ORDER_ID_MIN_LEN:
285
+ errors.append(
286
+ f"{name}.client_order_id_max_len={max_len} is below the wire "
287
+ f"floor ({WIRE_CLIENT_ORDER_ID_MIN_LEN}): the deterministic "
288
+ f"wire-form client-order-id needs 14 raw prefix characters plus "
289
+ f"a >=6-character hash tail to keep restart adoption sound. "
290
+ f"Venues with shorter client-id fields are not supportable."
291
+ )
292
+
293
+ # --- Override pairs ---
294
+ if (overridden('get_residual_orders_after_bracket_attach_reject')
295
+ and not overridden('cancel_broker_order_ref')):
296
+ errors.append(
297
+ f"{name} overrides get_residual_orders_after_bracket_attach_reject() "
298
+ f"but not cancel_broker_order_ref() — the defensive-close recovery "
299
+ f"loop passes every returned ref to cancel_broker_order_ref(), "
300
+ f"whose default raises NotImplementedError. Override both."
301
+ )
302
+
303
+ if not overridden('execute_cancel_with_outcome'):
304
+ warnings.append(
305
+ f"{name} does not override execute_cancel_with_outcome(): every "
306
+ f"cancel disposition collapses to UNKNOWN, so a cancel-tentative "
307
+ f"order can only resolve through a broker-pushed FILL/CANCEL "
308
+ f"event. Override it to classify the exchange's post-cancel "
309
+ f"disposition when the venue makes it readable."
310
+ )
311
+
312
+ # --- PositionPort surface ---
313
+ port = plugin.position_port
314
+ if port is not None:
315
+ missing = [m for m in _POSITION_PORT_METHODS
316
+ if not callable(getattr(port, m, None))]
317
+ if missing:
318
+ errors.append(
319
+ f"{name}.position_port opts into core one-way emulation but "
320
+ f"is missing PositionPort method(s): {', '.join(missing)}. "
321
+ f"The OneWayEmulator drives the plugin purely through this "
322
+ f"surface — implement all of them."
323
+ )
324
+
325
+ # --- SpotInventoryPort surface ---
326
+ spot_port = plugin.spot_inventory_port
327
+ if spot_port is not None:
328
+ missing: list[str] = [
329
+ m for m in _SPOT_INVENTORY_PORT_METHODS
330
+ if not callable(getattr(spot_port, m, None))
331
+ ]
332
+ missing.extend(
333
+ a for a in _SPOT_INVENTORY_PORT_ATTRS
334
+ if getattr(spot_port, a, None) is None
335
+ )
336
+ if missing:
337
+ errors.append(
338
+ f"{name}.spot_inventory_port opts into core spot inventory "
339
+ f"but is missing SpotInventoryPort member(s): "
340
+ f"{', '.join(missing)}. The SpotInventoryManager drives the "
341
+ f"venue purely through this surface — implement all of them."
342
+ )
343
+ if plugin.on_inventory_conflict not in ('quarantine', 'halt'):
344
+ errors.append(
345
+ f"{name}.on_inventory_conflict is "
346
+ f"{plugin.on_inventory_conflict!r} — must be 'quarantine' "
347
+ f"or 'halt' (the inventory-conflict policy set is narrower "
348
+ f"than on_unexpected_cancel by design)."
349
+ )
350
+ grace = getattr(spot_port, 'settlement_grace_s', None)
351
+ if (isinstance(grace, bool)
352
+ or not isinstance(grace, (int, float))
353
+ or not math.isfinite(grace)
354
+ or grace < 0):
355
+ errors.append(
356
+ f"{name}.spot_inventory_port.settlement_grace_s is "
357
+ f"{grace!r} — must be a finite non-negative real number "
358
+ f"(a NaN/inf grace would let a confirmed inventory "
359
+ f"conflict stay pending forever while trading continues)."
360
+ )
361
+ if ('short_selling' not in bad_fields
362
+ and caps.short_selling.is_supported):
363
+ errors.append(
364
+ f"{name} declares a spot_inventory_port AND a supported "
365
+ f"short_selling capability — the two are mutually "
366
+ f"exclusive. The spot ledger models long-only exposure "
367
+ f"(a negative base position cannot exist on a spot "
368
+ f"venue); a short-capable venue must not opt into core "
369
+ f"spot inventory."
370
+ )
371
+
372
+ # --- Lifecycle ---
373
+ if require_account_id and plugin.account_id == "default":
374
+ errors.append(
375
+ f"{name}.account_id is still the \"default\" sentinel after "
376
+ f"authentication — connect()/session setup must populate "
377
+ f"self._account_id BEFORE broker storage derives the run "
378
+ f"identity, otherwise every run of every account of this "
379
+ f"plugin collides on one identity."
380
+ )
381
+
382
+ return errors, warnings
@@ -0,0 +1,7 @@
1
+ # noinspection PyPep8Naming
2
+ class classproperty:
3
+ def __init__(self, f):
4
+ self.f = f
5
+
6
+ def __get__(self, obj, owner):
7
+ return self.f(owner)