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,334 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Translate the Pine ``strategy.*`` order book into broker intent objects.
|
|
3
|
+
|
|
4
|
+
The :class:`~pynecore.lib.strategy.SimPosition` maintains two dictionaries
|
|
5
|
+
of pending Pine orders created by ``strategy.entry``, ``strategy.order``,
|
|
6
|
+
``strategy.exit``, ``strategy.close`` and ``strategy.close_all``:
|
|
7
|
+
|
|
8
|
+
- ``position.entry_orders[pine_id]`` — entries and strategy-order adds
|
|
9
|
+
- ``position.exit_orders[(exit_id, from_entry)]`` — exits, closes and
|
|
10
|
+
close-all (composite key disambiguates multiple exits per entry)
|
|
11
|
+
|
|
12
|
+
For live trading the :class:`~pynecore.core.broker.sync_engine.OrderSyncEngine`
|
|
13
|
+
consumes these via :func:`build_intents`, which returns a flat list of
|
|
14
|
+
:class:`EntryIntent`, :class:`ExitIntent` and :class:`CloseIntent`
|
|
15
|
+
dataclasses the broker plugin can act on. The translation is **pure**:
|
|
16
|
+
no access to ``lib._script`` or ``syminfo``; the caller supplies the
|
|
17
|
+
symbol and keeps tick/mintick resolution inside the sync engine.
|
|
18
|
+
"""
|
|
19
|
+
from typing import Iterable
|
|
20
|
+
|
|
21
|
+
from pynecore.core.broker.models import (
|
|
22
|
+
EntryIntent,
|
|
23
|
+
ExitIntent,
|
|
24
|
+
CloseIntent,
|
|
25
|
+
OcaType,
|
|
26
|
+
OrderType,
|
|
27
|
+
)
|
|
28
|
+
# noinspection PyProtectedMember
|
|
29
|
+
from pynecore.lib.strategy import (
|
|
30
|
+
Order,
|
|
31
|
+
Trade,
|
|
32
|
+
_order_type_normal,
|
|
33
|
+
)
|
|
34
|
+
from pynecore.types.na import NA
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
'build_intents',
|
|
38
|
+
'build_entry_intent',
|
|
39
|
+
'build_exit_intent',
|
|
40
|
+
'build_close_intent',
|
|
41
|
+
'CLOSE_EXIT_ID_PREFIX',
|
|
42
|
+
'CLOSE_ALL_EXIT_ID',
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
# Prefixes set by ``strategy.close`` / ``strategy.close_all`` in
|
|
46
|
+
# ``lib/strategy/__init__.py``. Used to distinguish a CloseIntent from an
|
|
47
|
+
# ExitIntent when both share ``_order_type_close``.
|
|
48
|
+
CLOSE_EXIT_ID_PREFIX = "Close entry(s) order "
|
|
49
|
+
CLOSE_ALL_EXIT_ID = "Close position order"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _side_from_size(size: float) -> str:
|
|
53
|
+
"""Signed Pine size → ``"buy"``/``"sell"``.
|
|
54
|
+
|
|
55
|
+
Pine uses signed sizes: positive for long, negative for short. Exit/close
|
|
56
|
+
orders always carry the **opposite** sign of the position, so a long close
|
|
57
|
+
has a negative size → ``"sell"``, matching the exchange-side semantics.
|
|
58
|
+
"""
|
|
59
|
+
return "buy" if size > 0 else "sell"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _infer_order_type(limit: float | None, stop: float | None) -> OrderType:
|
|
63
|
+
"""Map a Pine entry's ``limit``/``stop`` prices to a broker order type.
|
|
64
|
+
|
|
65
|
+
Pine has no "stop-limit" entry. When BOTH prices are set the order is two
|
|
66
|
+
OCO legs: a LIMIT below the open (guaranteed-price pullback) and a STOP
|
|
67
|
+
above (market-on-rise). The sync engine realises the both-set case as a
|
|
68
|
+
native resting LIMIT plus a software price-watch on the stop side, so the
|
|
69
|
+
inferred type is LIMIT — both-set and limit-only share the same resting
|
|
70
|
+
leg. The ``EntryIntent`` still carries the ``stop`` price for the engine
|
|
71
|
+
to arm its watch.
|
|
72
|
+
"""
|
|
73
|
+
if limit is not None:
|
|
74
|
+
return OrderType.LIMIT
|
|
75
|
+
if stop is not None:
|
|
76
|
+
return OrderType.STOP
|
|
77
|
+
return OrderType.MARKET
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _na_to_none(value):
|
|
81
|
+
"""Strip Pine :class:`NA` markers; leave concrete values alone."""
|
|
82
|
+
return None if isinstance(value, NA) else value
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _coerce_oca(order: Order) -> tuple[str | None, str | None]:
|
|
86
|
+
"""Return a protocol-friendly ``(oca_name, oca_type)`` pair.
|
|
87
|
+
|
|
88
|
+
``Order`` always stores a non-None ``oca_type`` (defaults to :data:`oca.none`)
|
|
89
|
+
even when no OCA participation is requested. The intent layer uses
|
|
90
|
+
``None`` to mean "not in an OCA group" — so only emit ``oca_type`` when
|
|
91
|
+
the order actually names a group.
|
|
92
|
+
|
|
93
|
+
Unknown ``oca_type`` strings are rejected here rather than silently passed
|
|
94
|
+
through to the sync engine, where a typo would disable cascade cancel
|
|
95
|
+
without any diagnostic. The accepted values are exactly the members of
|
|
96
|
+
:class:`OcaType`.
|
|
97
|
+
"""
|
|
98
|
+
if order.oca_name is None:
|
|
99
|
+
return None, None
|
|
100
|
+
if order.oca_type is None:
|
|
101
|
+
return order.oca_name, None
|
|
102
|
+
oca_type_str = str(order.oca_type)
|
|
103
|
+
try:
|
|
104
|
+
OcaType(oca_type_str)
|
|
105
|
+
except ValueError as exc:
|
|
106
|
+
raise ValueError(
|
|
107
|
+
f"unknown oca_type {oca_type_str!r}; expected one of "
|
|
108
|
+
f"{[m.value for m in OcaType]}",
|
|
109
|
+
) from exc
|
|
110
|
+
return order.oca_name, oca_type_str
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def build_entry_intent(order: Order, symbol: str) -> EntryIntent:
|
|
114
|
+
"""Translate a ``strategy.entry`` / ``strategy.order`` Pine order.
|
|
115
|
+
|
|
116
|
+
The intent carries the RAW script quantity — deliberately. Pine keeps
|
|
117
|
+
a consumed market entry's ``Order`` in ``entry_orders`` (it is never
|
|
118
|
+
popped), so ``build_intents`` re-emits the same intent every bar and
|
|
119
|
+
the sync engine's diff relies on it being byte-stable to recognise an
|
|
120
|
+
already-dispatched entry. The MARKET-reversal stop-and-reverse fold
|
|
121
|
+
(TV: combined = qty + |opposite position|) therefore happens at
|
|
122
|
+
DISPATCH time in :meth:`OrderSyncEngine._dispatch_new`, which runs
|
|
123
|
+
exactly once per fresh intent — folding here would make a stale
|
|
124
|
+
retained order's qty flap with the net position and re-trigger the
|
|
125
|
+
diff (measured on the Capital.com demo E2E, 2026-07-10).
|
|
126
|
+
"""
|
|
127
|
+
oca_name, oca_type_str = _coerce_oca(order)
|
|
128
|
+
return EntryIntent(
|
|
129
|
+
pine_id=order.order_id or "",
|
|
130
|
+
symbol=symbol,
|
|
131
|
+
side=_side_from_size(float(order.size)),
|
|
132
|
+
qty=abs(order.size),
|
|
133
|
+
order_type=_infer_order_type(order.limit, order.stop),
|
|
134
|
+
limit=order.limit,
|
|
135
|
+
stop=order.stop,
|
|
136
|
+
oca_name=oca_name,
|
|
137
|
+
oca_type=oca_type_str,
|
|
138
|
+
comment=_na_to_none(order.comment),
|
|
139
|
+
alert_message=_na_to_none(order.alert_message),
|
|
140
|
+
is_strategy_order=(order.order_type == _order_type_normal),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def build_exit_intent(
|
|
145
|
+
order: Order,
|
|
146
|
+
symbol: str,
|
|
147
|
+
*,
|
|
148
|
+
parent_total_qty: float = 0.0,
|
|
149
|
+
) -> ExitIntent:
|
|
150
|
+
"""Translate a ``strategy.exit`` Pine order.
|
|
151
|
+
|
|
152
|
+
Tick-based exits (``profit=``/``loss=``/``trail_points=``) carry unresolved
|
|
153
|
+
distances: the plugin cannot place absolute TP/SL prices until the entry
|
|
154
|
+
fill price is known. The intent preserves the tick values
|
|
155
|
+
**alongside** empty ``tp_price``/``sl_price`` fields; the sync engine
|
|
156
|
+
resolves them on the corresponding entry fill event.
|
|
157
|
+
|
|
158
|
+
:param order: The Pine ``strategy.exit`` :class:`Order` to translate.
|
|
159
|
+
:param symbol: Exchange symbol the resulting :class:`ExitIntent` targets.
|
|
160
|
+
:param parent_total_qty: total open qty currently associated with the
|
|
161
|
+
``from_entry`` Pine id — used to set
|
|
162
|
+
:attr:`ExitIntent.is_partial_qty_bracket` when the script asks for a
|
|
163
|
+
bracket on a fraction of the parent. ``0.0`` (the default) leaves
|
|
164
|
+
the flag ``False``; ``build_intents`` supplies the live value from
|
|
165
|
+
``position.open_trades`` so the order sync engine can dispatch on
|
|
166
|
+
``caps.partial_qty_bracket_exit``.
|
|
167
|
+
"""
|
|
168
|
+
oca_name, oca_type_str = _coerce_oca(order)
|
|
169
|
+
# Tick values take priority over explicit prices — mirrors Pine's
|
|
170
|
+
# exit() fill-time logic where profit_ticks overwrites order.limit.
|
|
171
|
+
tp_price = order.limit if order.profit_ticks is None else None
|
|
172
|
+
sl_price = order.stop if order.loss_ticks is None else None
|
|
173
|
+
trail_price = order.trail_price if order.trail_points_ticks is None else None
|
|
174
|
+
has_trail = (
|
|
175
|
+
order.trail_price is not None or order.trail_points_ticks is not None
|
|
176
|
+
)
|
|
177
|
+
trail_offset = order.trail_offset if has_trail else None
|
|
178
|
+
has_bracket_leg = (
|
|
179
|
+
tp_price is not None
|
|
180
|
+
or sl_price is not None
|
|
181
|
+
or trail_price is not None
|
|
182
|
+
or trail_offset is not None
|
|
183
|
+
or order.profit_ticks is not None
|
|
184
|
+
or order.loss_ticks is not None
|
|
185
|
+
or order.trail_points_ticks is not None
|
|
186
|
+
)
|
|
187
|
+
exit_qty = abs(order.size)
|
|
188
|
+
# A bracket on the whole row stays on the full-row dispatch path even
|
|
189
|
+
# when the capability advertises a partial-aware mechanism — Pine
|
|
190
|
+
# semantics for ``qty == total`` are identical to the simple bracket.
|
|
191
|
+
is_partial = (
|
|
192
|
+
has_bracket_leg
|
|
193
|
+
and parent_total_qty > 0.0
|
|
194
|
+
and exit_qty + 1e-12 < parent_total_qty
|
|
195
|
+
)
|
|
196
|
+
return ExitIntent(
|
|
197
|
+
pine_id=order.exit_id or "",
|
|
198
|
+
from_entry=order.order_id or "",
|
|
199
|
+
symbol=symbol,
|
|
200
|
+
side=_side_from_size(float(order.size)),
|
|
201
|
+
qty=exit_qty,
|
|
202
|
+
tp_price=tp_price,
|
|
203
|
+
sl_price=sl_price,
|
|
204
|
+
trail_price=trail_price,
|
|
205
|
+
trail_offset=trail_offset,
|
|
206
|
+
profit_ticks=order.profit_ticks,
|
|
207
|
+
loss_ticks=order.loss_ticks,
|
|
208
|
+
trail_points_ticks=order.trail_points_ticks,
|
|
209
|
+
oca_name=oca_name,
|
|
210
|
+
oca_type=oca_type_str,
|
|
211
|
+
comment=_na_to_none(order.comment),
|
|
212
|
+
comment_profit=_na_to_none(order.comment_profit),
|
|
213
|
+
comment_loss=_na_to_none(order.comment_loss),
|
|
214
|
+
comment_trailing=_na_to_none(order.comment_trailing),
|
|
215
|
+
alert_message=_na_to_none(order.alert_message),
|
|
216
|
+
is_partial_qty_bracket=is_partial,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def build_close_intent(order: Order, symbol: str, *, is_close_all: bool) -> CloseIntent:
|
|
221
|
+
"""Translate ``strategy.close(id)`` or ``strategy.close_all()``.
|
|
222
|
+
|
|
223
|
+
``strategy.close_all()`` uses an empty ``pine_id`` — the Pine order itself
|
|
224
|
+
has ``order_id=None`` because the close targets the whole position rather
|
|
225
|
+
than a specific entry identifier.
|
|
226
|
+
"""
|
|
227
|
+
pine_id = "" if is_close_all else (order.order_id or "")
|
|
228
|
+
return CloseIntent(
|
|
229
|
+
pine_id=pine_id,
|
|
230
|
+
symbol=symbol,
|
|
231
|
+
side=_side_from_size(float(order.size)),
|
|
232
|
+
qty=abs(order.size),
|
|
233
|
+
immediately=False,
|
|
234
|
+
comment=_na_to_none(order.comment),
|
|
235
|
+
alert_message=_na_to_none(order.alert_message),
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _classify_exit_side(order: Order) -> str:
|
|
240
|
+
"""``'close_all' | 'close' | 'exit'`` — the exit_orders dict is polymorphic."""
|
|
241
|
+
if order.exit_id == CLOSE_ALL_EXIT_ID:
|
|
242
|
+
return 'close_all'
|
|
243
|
+
if order.exit_id and order.exit_id.startswith(CLOSE_EXIT_ID_PREFIX):
|
|
244
|
+
return 'close'
|
|
245
|
+
return 'exit'
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def build_intents(
|
|
249
|
+
entry_orders: dict,
|
|
250
|
+
exit_orders: dict,
|
|
251
|
+
symbol: str,
|
|
252
|
+
open_trades: Iterable[Trade] | None = None,
|
|
253
|
+
) -> list[EntryIntent | ExitIntent | CloseIntent]:
|
|
254
|
+
"""Flatten a position's pending orders into intent objects.
|
|
255
|
+
|
|
256
|
+
``entry_orders`` and ``exit_orders`` are the ``dict``s that
|
|
257
|
+
:class:`~pynecore.lib.strategy.SimPosition` exposes. Orders already
|
|
258
|
+
marked ``cancelled`` (e.g. via OCA or :func:`strategy.cancel`) are
|
|
259
|
+
filtered out — the caller treats their absence as an implicit cancel.
|
|
260
|
+
|
|
261
|
+
``open_trades`` is ``position.open_trades`` — used solely to compute the
|
|
262
|
+
total open qty per ``from_entry`` for the partial-qty bracket flag on
|
|
263
|
+
:class:`ExitIntent`. Optional; when omitted the flag stays ``False``
|
|
264
|
+
and the exit falls back to the full-row dispatch path (the safe
|
|
265
|
+
default for unit tests and simulator-only callers that don't carry a
|
|
266
|
+
live trade ledger).
|
|
267
|
+
"""
|
|
268
|
+
intents: list[EntryIntent | ExitIntent | CloseIntent] = []
|
|
269
|
+
|
|
270
|
+
qty_by_entry: dict[str, float] = {}
|
|
271
|
+
count_by_entry: dict[str, int] = {}
|
|
272
|
+
if open_trades is not None:
|
|
273
|
+
for trade in open_trades:
|
|
274
|
+
entry_id = trade.entry_id
|
|
275
|
+
if entry_id is None:
|
|
276
|
+
continue
|
|
277
|
+
qty_by_entry[entry_id] = (
|
|
278
|
+
qty_by_entry.get(entry_id, 0.0) + abs(trade.size)
|
|
279
|
+
)
|
|
280
|
+
count_by_entry[entry_id] = count_by_entry.get(entry_id, 0) + 1
|
|
281
|
+
|
|
282
|
+
for order in _active(entry_orders.values()):
|
|
283
|
+
intents.append(build_entry_intent(order, symbol))
|
|
284
|
+
|
|
285
|
+
for order in _active(exit_orders.values()):
|
|
286
|
+
kind = _classify_exit_side(order)
|
|
287
|
+
if kind == 'close_all':
|
|
288
|
+
intents.append(build_close_intent(order, symbol, is_close_all=True))
|
|
289
|
+
elif kind == 'close':
|
|
290
|
+
intents.append(build_close_intent(order, symbol, is_close_all=False))
|
|
291
|
+
else:
|
|
292
|
+
from_entry = order.order_id or ""
|
|
293
|
+
# ``parent_total_qty`` drives ``ExitIntent.is_partial_qty_bracket``
|
|
294
|
+
# (the partial-vs-whole-row dispatch). It is sourced by the open
|
|
295
|
+
# row count under ``from_entry``. This relies on a simulator
|
|
296
|
+
# invariant: ``build_intents`` reads ``SimPosition.open_trades``,
|
|
297
|
+
# where each Pine entry event is exactly one ``Trade`` row (the
|
|
298
|
+
# simulator has no split fills — an order fills in full when its
|
|
299
|
+
# condition is met).
|
|
300
|
+
#
|
|
301
|
+
# * >1 row — pyramiding or repeated ``strategy.order`` under one
|
|
302
|
+
# Pine id. ``entry_orders[from_entry]`` keeps only the LATEST
|
|
303
|
+
# same-id row (``_add_order`` overwrites the dict slot), so the
|
|
304
|
+
# declared size understates the parent and would misclassify a
|
|
305
|
+
# partial exit as whole-row, mis-hedging the older legs. The
|
|
306
|
+
# summed open qty is the only faithful total here — and it is
|
|
307
|
+
# exactly what Pine's ``strategy.exit(qty=N, from_entry=...)``
|
|
308
|
+
# measures N against.
|
|
309
|
+
# * ==1 row — single-row: use the script-declared size. It is the
|
|
310
|
+
# stable "what the script asked for" signal and survives the
|
|
311
|
+
# fill (``record_fill`` only touches ``open_trades``, never pops
|
|
312
|
+
# the entry Order), so a broker overfill/partial-fill cannot flip
|
|
313
|
+
# the flag mid-lifecycle and trigger a spurious native<->partial
|
|
314
|
+
# bracket cancel/re-arm.
|
|
315
|
+
# * no declared entry left (cancelled / cleared) — fall back to the
|
|
316
|
+
# actual open qty.
|
|
317
|
+
declared_entry: Order | None = entry_orders.get(from_entry)
|
|
318
|
+
if count_by_entry.get(from_entry, 0) > 1:
|
|
319
|
+
parent_total_qty = qty_by_entry[from_entry]
|
|
320
|
+
elif declared_entry is not None:
|
|
321
|
+
parent_total_qty = abs(float(declared_entry.size))
|
|
322
|
+
else:
|
|
323
|
+
parent_total_qty = qty_by_entry.get(from_entry, 0.0)
|
|
324
|
+
intents.append(build_exit_intent(
|
|
325
|
+
order, symbol, parent_total_qty=parent_total_qty,
|
|
326
|
+
))
|
|
327
|
+
|
|
328
|
+
return intents
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _active(orders: Iterable[Order]) -> Iterable[Order]:
|
|
332
|
+
for order in orders:
|
|
333
|
+
if not order.cancelled:
|
|
334
|
+
yield order
|