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,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
|