opencode-pyneruntime 6.8.5__tar.gz → 6.8.7__tar.gz
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.8.5 → opencode_pyneruntime-6.8.7}/PKG-INFO +12 -5
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/README.md +11 -4
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/pyproject.toml +1 -1
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/cli/commands/run.py +6 -2
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/intent_builder.py +6 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/models.py +271 -4
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/one_way_emulator.py +103 -9
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/store_helpers.py +115 -1
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/sync_engine.py +1628 -79
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/datetime.py +47 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/import_hook.py +2 -2
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/instance_state.py +150 -2
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/live_runner.py +371 -35
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/plugin/live_provider.py +49 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/script_runner.py +323 -60
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/security.py +23 -7
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/__init__.py +450 -66
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/_timeframe_change.py +6 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/chart.py +3 -3
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/linefill.py +12 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/math.py +30 -13
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/polyline.py +2 -2
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/session.py +32 -26
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/strategy/__init__.py +971 -295
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/strategy/opentrades.py +1 -1
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/ta.py +123 -59
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/table.py +1 -1
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/function_isolation.py +24 -3
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/module_properties.json +15 -9
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/strategy.py +15 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/LICENSE +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/MANIFEST.in +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/NOTICE +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/build/opencode_pyneruntime.egg-info/SOURCES.txt +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/setup.cfg +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/__init__.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/cli/__init__.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/cli/app.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/cli/commands/__init__.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/cli/commands/benchmark.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/cli/commands/compile.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/cli/commands/data.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/cli/commands/debug.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/cli/commands/optimize.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/cli/commands/plugin.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/cli/pluggable.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/cli/utils/__init__.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/cli/utils/api_error_handler.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/cli/utils/broker_picker.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/cli/utils/error_hook.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/cli/utils/keyreader.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/cli/utils/provider_picker.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/cli/utils/symbol_browser.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/__init__.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/_file_io.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/aggregator.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/bar_magnifier.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/__init__.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/defaults.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/disappearance.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/emulator.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/exceptions.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/idempotency.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/journal.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/native_failsafe_manager.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/position.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/run_identity.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/software_entry_stop_engine.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/software_partial_bracket_engine.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/spot_inventory.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/storage.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/validation.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/class_property.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/config.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/csv_file.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/currency.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/data_converter.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/download_info.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/download_runner.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/drawing_snapshot.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/htf_aggregator.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/live_ltf_collector.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/live_ltf_window.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/module_property.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/ohlcv.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/ohlcv_importers.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/ohlcv_legacy.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/overload.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/pine_cast.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/pine_compare.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/pine_export.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/pine_method.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/pine_range.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/pine_udt.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/plugin/__init__.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/plugin/broker.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/plugin/cli.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/plugin/provider.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/provider_string.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/random.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/resampler.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/rolling_sum.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/safe_convert.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/script.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/security_process.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/security_shm.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/series.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/session.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/strategy_stats.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/symbol_map.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/syminfo.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/viz.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/_fixnan.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/_math_stateful.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/adjustment.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/alert.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/alert.pyi +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/array.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/barmerge.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/barstate.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/box.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/color.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/color.pyi +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/currency.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/dayofweek.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/dayofweek.pyi +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/display.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/dividends.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/earnings.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/extend.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/font.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/footprint.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/format.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/hline.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/hline.pyi +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/label.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/line.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/location.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/log.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/map.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/matrix.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/order.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/pivotpointtype.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/plot.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/plot.pyi +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/position.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/request.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/runtime.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/scale.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/session.pyi +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/shape.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/size.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/splits.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/strategy/closedtrades.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/strategy/closedtrades.pyi +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/strategy/commission.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/strategy/direction.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/strategy/oca.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/strategy/opentrades.pyi +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/strategy/risk.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/string.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/syminfo.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/text.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/ticker.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/timeframe.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/volume_row.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/xloc.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/yloc.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/providers/__init__.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/providers/ccxt.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/providers/replay.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/pynesys/__init__.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/pynesys/api.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/pynesys/compiler.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/standalone.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/testing/__init__.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/testing/broker_lab/__init__.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/testing/broker_lab/__main__.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/testing/broker_lab/cli.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/testing/broker_lab/generate.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/testing/broker_lab/model.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/testing/broker_lab/reference.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/testing/broker_lab/runner.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/testing/broker_lab/scheduler.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/testing/broker_lab/subprocess.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/__init__.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/builtin_shadow.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/closure_arguments_transformer.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/display_rewrite.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/dynamic_default.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/float_tolerance.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/import_lifter.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/import_normalizer.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/inline_series_hoist.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/input_transformer.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/lib_series.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/locations.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/module_property.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/persistent.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/persistent_series.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/pine_truthiness.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/safe_convert_transformer.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/safe_division_transformer.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/script_requirements.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/security.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/security_instantiation.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/series.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/slot_layout.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/type_checking_stripper.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/unused_series_detector.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/__init__.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/alert.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/barmerge.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/base.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/box.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/chart.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/color.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/currency.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/datetime.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/display.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/dividends.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/earnings.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/extend.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/font.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/footprint.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/format.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/hline.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/ib_persistent.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/ib_persistent.pyi +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/label.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/line.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/linefill.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/location.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/matrix.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/na.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/na.pyi +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/ohlcv.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/order.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/persistent.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/persistent.pyi +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/pine_types.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/pine_types.pyi +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/pivotpointtype.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/plot.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/plot_meta.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/polyline.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/position.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/scale.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/script_type.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/series.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/series.pyi +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/session.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/shape.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/size.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/source.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/splits.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/table.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/text.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/type_checker.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/type_checker.pyi +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/volume_row.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/weekdays.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/xloc.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/yloc.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/utils/__init__.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/utils/file_utils.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/utils/rich/__init__.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/utils/rich/date_column.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/utils/sequence_view.py +0 -0
- {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/utils/stdlib_checker.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: opencode-pyneruntime
|
|
3
|
-
Version: 6.8.
|
|
3
|
+
Version: 6.8.7
|
|
4
4
|
Summary: PyneCore runtime (rubycell fork of PyneSys/pynecore) — Pine Script-like runtime and API
|
|
5
5
|
Author-email: PYNESYS LLC <hello@pynesys.com>
|
|
6
6
|
Maintainer: rubycell
|
|
@@ -224,15 +224,22 @@ auto-discovered through the `pyne.plugin` entry-point group:
|
|
|
224
224
|
| **DNSE** — [`plugins/dnse`](plugins/dnse) | `dnse` (data) · `dnse_broker` (broker) | Vietnamese derivatives & stocks: OHLCV history + **native STOP/OCO conditional orders** on DNSE's OpenAPI, built on the vendored `openapi-sdk` 2.0.0. REST-only. |
|
|
225
225
|
|
|
226
226
|
```bash
|
|
227
|
-
# historical / live data feed
|
|
228
|
-
|
|
227
|
+
# historical / live data feed (no --from: it truncates the shared .ohlcv cache;
|
|
228
|
+
# the built-in -500-real-bars default is both safe and deep enough)
|
|
229
|
+
pyne run my_strategy.py dnse:VN30F1M@5
|
|
229
230
|
|
|
230
231
|
# live broker — places REAL orders (implies --live)
|
|
231
232
|
pyne run my_strategy.py dnse_broker:VN30F1M@5 --broker
|
|
232
233
|
```
|
|
233
234
|
|
|
234
|
-
|
|
235
|
-
|
|
235
|
+
Testing: `pytest plugins/dnse/tests/` (unit + offline e2e against a fake venue that
|
|
236
|
+
reproduces DNSE's measured quirks), plus a live-test suite — a session-independent
|
|
237
|
+
venue-semantics gate and two staged live probes (no-fill place/amend/cancel/OCA cases
|
|
238
|
+
and a fill matrix whose backtest mode doubles as the oracle). Procedures, safety rules
|
|
239
|
+
and the measured venue facts: [`plugins/dnse/testing/live_test/README.md`](plugins/dnse/testing/live_test/README.md).
|
|
240
|
+
|
|
241
|
+
The vendored SDK, a mirror of DNSE's OpenAPI docs
|
|
242
|
+
(`docs/dnse-openapi-documentation/`) with a sync/verify tool, and the
|
|
236
243
|
error-handling plan (`docs/plan/`) live alongside the plugin.
|
|
237
244
|
|
|
238
245
|
**Reference plugins** — the DNSE plugin follows PyneSys's official broker-plugin
|
|
@@ -179,15 +179,22 @@ auto-discovered through the `pyne.plugin` entry-point group:
|
|
|
179
179
|
| **DNSE** — [`plugins/dnse`](plugins/dnse) | `dnse` (data) · `dnse_broker` (broker) | Vietnamese derivatives & stocks: OHLCV history + **native STOP/OCO conditional orders** on DNSE's OpenAPI, built on the vendored `openapi-sdk` 2.0.0. REST-only. |
|
|
180
180
|
|
|
181
181
|
```bash
|
|
182
|
-
# historical / live data feed
|
|
183
|
-
|
|
182
|
+
# historical / live data feed (no --from: it truncates the shared .ohlcv cache;
|
|
183
|
+
# the built-in -500-real-bars default is both safe and deep enough)
|
|
184
|
+
pyne run my_strategy.py dnse:VN30F1M@5
|
|
184
185
|
|
|
185
186
|
# live broker — places REAL orders (implies --live)
|
|
186
187
|
pyne run my_strategy.py dnse_broker:VN30F1M@5 --broker
|
|
187
188
|
```
|
|
188
189
|
|
|
189
|
-
|
|
190
|
-
|
|
190
|
+
Testing: `pytest plugins/dnse/tests/` (unit + offline e2e against a fake venue that
|
|
191
|
+
reproduces DNSE's measured quirks), plus a live-test suite — a session-independent
|
|
192
|
+
venue-semantics gate and two staged live probes (no-fill place/amend/cancel/OCA cases
|
|
193
|
+
and a fill matrix whose backtest mode doubles as the oracle). Procedures, safety rules
|
|
194
|
+
and the measured venue facts: [`plugins/dnse/testing/live_test/README.md`](plugins/dnse/testing/live_test/README.md).
|
|
195
|
+
|
|
196
|
+
The vendored SDK, a mirror of DNSE's OpenAPI docs
|
|
197
|
+
(`docs/dnse-openapi-documentation/`) with a sync/verify tool, and the
|
|
191
198
|
error-handling plan (`docs/plan/`) live alongside the plugin.
|
|
192
199
|
|
|
193
200
|
**Reference plugins** — the DNSE plugin follows PyneSys's official broker-plugin
|
|
@@ -20,7 +20,7 @@ egg_info.egg_base = "build"
|
|
|
20
20
|
# `pynecore` so transpiler-generated code runs unchanged. Upstream authorship and
|
|
21
21
|
# the Apache-2.0 NOTICE ("Powered by PyneSys") are retained per Section 4(d).
|
|
22
22
|
name = "opencode-pyneruntime"
|
|
23
|
-
version = "6.8.
|
|
23
|
+
version = "6.8.7" # PineVersion.Major.Minor — tracks upstream
|
|
24
24
|
description = "PyneCore runtime (rubycell fork of PyneSys/pynecore) — Pine Script-like runtime and API"
|
|
25
25
|
authors = [{ name = "PYNESYS LLC", email = "hello@pynesys.com" }]
|
|
26
26
|
maintainers = [{ name = "rubycell" }]
|
|
@@ -1005,8 +1005,12 @@ def _print_data_requirements(requirements: DataRequirements, script_name: str) -
|
|
|
1005
1005
|
lines.append("")
|
|
1006
1006
|
lines.append("Same symbol, other timeframe (resampled from the chart base data):")
|
|
1007
1007
|
for r in requirements.same_symbol_other_tf:
|
|
1008
|
-
|
|
1009
|
-
|
|
1008
|
+
if r.has_security_mapping:
|
|
1009
|
+
suffix = " [mapping present]"
|
|
1010
|
+
elif r.derived_from_chart:
|
|
1011
|
+
suffix = " [served from the chart data, nothing to supply]"
|
|
1012
|
+
else:
|
|
1013
|
+
suffix = f" [needs --security '{r.timeframe}=<base file>' in backtest]"
|
|
1010
1014
|
lines.append(f" -> {r.symbol} @ {r.timeframe}{_tags(r)}{suffix}")
|
|
1011
1015
|
|
|
1012
1016
|
if requirements.cross_symbol:
|
{opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/intent_builder.py
RENAMED
|
@@ -123,6 +123,11 @@ def build_entry_intent(order: Order, symbol: str) -> EntryIntent:
|
|
|
123
123
|
exactly once per fresh intent — folding here would make a stale
|
|
124
124
|
retained order's qty flap with the net position and re-trigger the
|
|
125
125
|
diff (measured on the Capital.com demo E2E, 2026-07-10).
|
|
126
|
+
|
|
127
|
+
``Order.skip_flip`` rides along so that fold can be suppressed for an entry
|
|
128
|
+
re-placed on its own bar: TV modifies the standing order with the raw
|
|
129
|
+
quantity and never recomputes the flip, so folding it at dispatch would
|
|
130
|
+
reverse a position the script only meant to reduce.
|
|
126
131
|
"""
|
|
127
132
|
oca_name, oca_type_str = _coerce_oca(order)
|
|
128
133
|
return EntryIntent(
|
|
@@ -138,6 +143,7 @@ def build_entry_intent(order: Order, symbol: str) -> EntryIntent:
|
|
|
138
143
|
comment=_na_to_none(order.comment),
|
|
139
144
|
alert_message=_na_to_none(order.alert_message),
|
|
140
145
|
is_strategy_order=(order.order_type == _order_type_normal),
|
|
146
|
+
skip_flip=order.skip_flip,
|
|
141
147
|
)
|
|
142
148
|
|
|
143
149
|
|
{opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/models.py
RENAMED
|
@@ -546,6 +546,16 @@ class EntryIntent:
|
|
|
546
546
|
# keeps it out of the diff equality: it is a dispatch route selector derived
|
|
547
547
|
# by the engine, not Pine-level state the diff needs to sync.
|
|
548
548
|
stop_fired_market: bool = field(default=False, compare=False)
|
|
549
|
+
# ``True`` when the Pine ``Order`` behind this intent re-placed an unfilled
|
|
550
|
+
# same-bar entry under the same id. TradingView modifies the standing order
|
|
551
|
+
# with the RAW quantity — the reversal flip the replaced order carried is not
|
|
552
|
+
# computed again — so the MARKET stop-and-reverse fold in
|
|
553
|
+
# :meth:`~pynecore.core.broker.sync_engine.OrderSyncEngine._dispatch_new`
|
|
554
|
+
# must be skipped for it, exactly as ``strategy.entry`` skips it in the
|
|
555
|
+
# simulator. ``compare=False`` for the same reason as above: it is a dispatch
|
|
556
|
+
# route selector, and letting it flip the diff would re-dispatch an entry the
|
|
557
|
+
# broker already holds.
|
|
558
|
+
skip_flip: bool = field(default=False, compare=False)
|
|
549
559
|
|
|
550
560
|
@property
|
|
551
561
|
def intent_key(self) -> str:
|
|
@@ -682,12 +692,45 @@ class CloseIntent:
|
|
|
682
692
|
# Same invariant as :attr:`ExitIntent.reduce_only` — a close can never
|
|
683
693
|
# flip the book to the other side in one-way Pine mode.
|
|
684
694
|
reduce_only: bool = True
|
|
695
|
+
# Engine-owned routing provenance. ``None`` means a script close;
|
|
696
|
+
# synthetic close producers set this explicitly so broker plugins never
|
|
697
|
+
# infer authority from a Pine-controlled string prefix.
|
|
698
|
+
synthetic_kind: str | None = None
|
|
699
|
+
target_entry_id: str | None = None
|
|
700
|
+
target_position_coid: str | None = None
|
|
701
|
+
target_exchange_id: str | None = None
|
|
685
702
|
|
|
686
703
|
def __post_init__(self) -> None:
|
|
687
704
|
if not self.reduce_only:
|
|
688
705
|
raise ValueError(
|
|
689
706
|
"CloseIntent.reduce_only must be True — one-way Pine semantics."
|
|
690
707
|
)
|
|
708
|
+
if self.synthetic_kind is None:
|
|
709
|
+
if (self.target_entry_id is not None
|
|
710
|
+
or self.target_position_coid is not None
|
|
711
|
+
or self.target_exchange_id is not None):
|
|
712
|
+
raise ValueError(
|
|
713
|
+
"CloseIntent synthetic targets require synthetic_kind."
|
|
714
|
+
)
|
|
715
|
+
return
|
|
716
|
+
if self.synthetic_kind not in {
|
|
717
|
+
'partial_trigger', 'marketable_exit', 'defensive_close',
|
|
718
|
+
}:
|
|
719
|
+
raise ValueError(
|
|
720
|
+
f"CloseIntent.synthetic_kind is invalid: {self.synthetic_kind!r}"
|
|
721
|
+
)
|
|
722
|
+
if self.synthetic_kind in {'partial_trigger', 'marketable_exit'}:
|
|
723
|
+
if (not self.target_entry_id
|
|
724
|
+
or self.target_position_coid is not None
|
|
725
|
+
or self.target_exchange_id is not None):
|
|
726
|
+
raise ValueError(
|
|
727
|
+
f"CloseIntent {self.synthetic_kind} requires target_entry_id only."
|
|
728
|
+
)
|
|
729
|
+
elif (not self.target_position_coid
|
|
730
|
+
or self.target_entry_id is not None):
|
|
731
|
+
raise ValueError(
|
|
732
|
+
"CloseIntent defensive_close requires position COID and no entry target."
|
|
733
|
+
)
|
|
691
734
|
|
|
692
735
|
@property
|
|
693
736
|
def intent_key(self) -> str:
|
|
@@ -963,10 +1006,13 @@ class PendingDefensiveClose:
|
|
|
963
1006
|
qty (which would double-subtract the partial slices already
|
|
964
1007
|
accounted for).
|
|
965
1008
|
:ivar unapplied_partial_qty: Cumulative ``fill_qty`` from no-FIFO
|
|
966
|
-
defensive-close
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1009
|
+
defensive-close events that the engine could NOT apply to
|
|
1010
|
+
:attr:`OrderSyncEngine._position.size` because the parent ENTRY
|
|
1011
|
+
fill had not yet arrived (``_position.size == 0.0``) — every
|
|
1012
|
+
``partial``, plus the terminal event of a leg that leaves a
|
|
1013
|
+
fanned close INCOMPLETE (that parent is not neutralised, so its
|
|
1014
|
+
fill still has to be booked and reduced).
|
|
1015
|
+
Tracked separately from :attr:`partial_filled_qty` so
|
|
970
1016
|
the terminal computation does not believe the slice was
|
|
971
1017
|
already booked. When the parent ENTRY ``filled`` / ``partial``
|
|
972
1018
|
event finally routes through
|
|
@@ -1025,6 +1071,92 @@ class PendingDefensiveClose:
|
|
|
1025
1071
|
produced no FIFO closures (no-FIFO / degenerate path) or when
|
|
1026
1072
|
the marker is replayed from an older schema; the retry path
|
|
1027
1073
|
falls back to ``marker.entry_id`` in that case.
|
|
1074
|
+
:ivar expected_close_qty: Total quantity the close was actually
|
|
1075
|
+
dispatched for when the one-way emulator fanned it across more
|
|
1076
|
+
than one hedge leg, in PINE units (``CloseFanResult`` reports it
|
|
1077
|
+
as ``dispatched_qty``; its ``legs`` volumes sit on the broker's
|
|
1078
|
+
integer grid and are not comparable with fill quantities) and
|
|
1079
|
+
already net of a broker-holds-less shortfall. Each leg is
|
|
1080
|
+
a separate broker order with its own terminal event, so the
|
|
1081
|
+
first leg FILL proves nothing about its siblings: while
|
|
1082
|
+
:attr:`applied_close_qty` is short of this value the marker
|
|
1083
|
+
stays armed (and the shared parent identities stay out of the
|
|
1084
|
+
settled-duplicate caches) so a sibling that is later rejected
|
|
1085
|
+
still escalates. ``None`` for a single-order close — the
|
|
1086
|
+
marker then settles on its one terminal FILL as before.
|
|
1087
|
+
:ivar applied_close_qty: Cumulative close quantity the broker has
|
|
1088
|
+
reported against this marker — the single authoritative counter
|
|
1089
|
+
for fan completion. Every booked slice is credited when it is
|
|
1090
|
+
observed: ``partial`` events (which carry a leg's intermediate
|
|
1091
|
+
segments), terminal ``filled`` events, and slices deferred on
|
|
1092
|
+
:attr:`unapplied_partial_qty` while the parent entry fill is
|
|
1093
|
+
still missing (the later drain does NOT credit them again). A
|
|
1094
|
+
slice the engine drops without booking anything (the
|
|
1095
|
+
``record_fill`` gate of positive qty AND price on the FIFO path,
|
|
1096
|
+
a zero-qty event on the no-FIFO path) is NOT credited, so a short
|
|
1097
|
+
total keeps the marker armed.
|
|
1098
|
+
:ivar settled_leg_order_ids: Broker-side ``order.id`` of every fan
|
|
1099
|
+
leg whose FILL was already applied to this marker. Only
|
|
1100
|
+
populated for a fanned close (``expected_close_qty is not
|
|
1101
|
+
None``), where the shared parent identities must stay OUT of the
|
|
1102
|
+
settled-duplicate caches until the last leg lands — so the only
|
|
1103
|
+
durable per-leg dedup identity is the leg's own order id.
|
|
1104
|
+
Startup replay seeds
|
|
1105
|
+
:attr:`OrderSyncEngine._settled_defensive_close_order_refs` from
|
|
1106
|
+
this tuple for an incomplete fan, keeping a delayed WS /
|
|
1107
|
+
polled-orders replay of an already-applied leg out of
|
|
1108
|
+
:meth:`BrokerPosition.record_fill` while the remaining legs are
|
|
1109
|
+
still outstanding.
|
|
1110
|
+
:ivar expected_leg_count: Number of legs the fan actually dispatched
|
|
1111
|
+
(``len(CloseFanResult.legs)``), ``0`` for a single-order close.
|
|
1112
|
+
A second, quantity-independent completion signal for
|
|
1113
|
+
:attr:`expected_close_qty`: that value is the PINE-unit plan sum,
|
|
1114
|
+
while the broker executes the plan snapped to its own integer
|
|
1115
|
+
volume grid, so the fills can total slightly less than the plan
|
|
1116
|
+
(a floor-style quantizer drops the sub-grid remainder). The
|
|
1117
|
+
quantity check alone would then never be satisfied and the marker
|
|
1118
|
+
would stay armed to the stale grace even though every dispatched
|
|
1119
|
+
leg filled. Once :attr:`settled_leg_order_ids` reaches this count
|
|
1120
|
+
every dispatched leg has reported its terminal FILL, so the fan is
|
|
1121
|
+
complete regardless of the residual quantity difference.
|
|
1122
|
+
:ivar booked_leg_count: How many fan legs have reported a terminal
|
|
1123
|
+
event that actually BOOKED quantity against this marker. Compared
|
|
1124
|
+
with :attr:`expected_leg_count` by
|
|
1125
|
+
:meth:`OrderSyncEngine._is_defensive_close_fan_incomplete`. Legs
|
|
1126
|
+
whose terminal event carried no quantity are deliberately NOT
|
|
1127
|
+
counted: they say nothing about the outstanding sibling quantity
|
|
1128
|
+
the marker exists to protect.
|
|
1129
|
+
:ivar dropped_close_qty: Cumulative close quantity the broker reported
|
|
1130
|
+
but the engine could NOT book — a ``partial`` slice with positive
|
|
1131
|
+
``fill_qty`` and a missing/zero ``fill_price``, which
|
|
1132
|
+
:meth:`BrokerPosition.record_fill` discards. Such a slice makes the
|
|
1133
|
+
local view short of the broker's real execution, and the leg-count
|
|
1134
|
+
completion signal cannot see it (every leg may still report
|
|
1135
|
+
terminally). While this is non-zero
|
|
1136
|
+
:meth:`OrderSyncEngine._is_defensive_close_fan_incomplete` therefore
|
|
1137
|
+
ignores :attr:`booked_leg_count` and falls back to the quantity
|
|
1138
|
+
signal, keeping the marker armed so the stale grace or the next
|
|
1139
|
+
reconcile escalates the divergence instead of silently retiring it.
|
|
1140
|
+
Only STILL-UNRESOLVED drops are counted: a corrected redelivery of
|
|
1141
|
+
the same execution subtracts its quantity again (see
|
|
1142
|
+
:attr:`dropped_fill_slices`).
|
|
1143
|
+
:ivar dropped_fill_slices: The still-unresolved drops behind
|
|
1144
|
+
:attr:`dropped_close_qty`, as ``(fill_id, qty)`` pairs. A dropped
|
|
1145
|
+
slice is NOT necessarily permanent: the engine deliberately keeps
|
|
1146
|
+
an unbookable fill's id out of
|
|
1147
|
+
:attr:`OrderSyncEngine._seen_fill_ids`
|
|
1148
|
+
(:meth:`OrderSyncEngine._is_duplicate_fill`) so the broker can
|
|
1149
|
+
redeliver the same execution with a corrected price, and that
|
|
1150
|
+
redelivery books the quantity after all. When it arrives, the
|
|
1151
|
+
matching pair is removed here and its quantity is subtracted from
|
|
1152
|
+
:attr:`dropped_close_qty`, re-arming the leg-count completion
|
|
1153
|
+
signal — without this the aggregate would stay positive forever
|
|
1154
|
+
and keep a fully settled off-grid fan armed to the stale grace.
|
|
1155
|
+
Slices dropped without a ``fill_id`` cannot be discharged (nothing
|
|
1156
|
+
identifies their redelivery) and are counted in the aggregate
|
|
1157
|
+
only, which stays conservative. The pairs also make the drop
|
|
1158
|
+
bookkeeping idempotent: a redelivered price-less slice with an
|
|
1159
|
+
already-recorded id is not counted twice.
|
|
1028
1160
|
"""
|
|
1029
1161
|
entry_id: str
|
|
1030
1162
|
close_intent_key: str
|
|
@@ -1039,6 +1171,13 @@ class PendingDefensiveClose:
|
|
|
1039
1171
|
pre_close_position_size: float | None = None
|
|
1040
1172
|
fifo_closed_entry_ids: tuple[str, ...] = ()
|
|
1041
1173
|
unapplied_partial_qty: float = 0.0
|
|
1174
|
+
expected_close_qty: float | None = None
|
|
1175
|
+
applied_close_qty: float = 0.0
|
|
1176
|
+
settled_leg_order_ids: tuple[str, ...] = ()
|
|
1177
|
+
expected_leg_count: int = 0
|
|
1178
|
+
booked_leg_count: int = 0
|
|
1179
|
+
dropped_close_qty: float = 0.0
|
|
1180
|
+
dropped_fill_slices: tuple[tuple[str, float], ...] = ()
|
|
1042
1181
|
|
|
1043
1182
|
def to_extras_dict(self) -> dict:
|
|
1044
1183
|
"""Serialize to a JSON-compatible dict for ``extras`` storage."""
|
|
@@ -1056,6 +1195,15 @@ class PendingDefensiveClose:
|
|
|
1056
1195
|
'pre_close_position_size': self.pre_close_position_size,
|
|
1057
1196
|
'fifo_closed_entry_ids': list(self.fifo_closed_entry_ids),
|
|
1058
1197
|
'unapplied_partial_qty': self.unapplied_partial_qty,
|
|
1198
|
+
'expected_close_qty': self.expected_close_qty,
|
|
1199
|
+
'applied_close_qty': self.applied_close_qty,
|
|
1200
|
+
'settled_leg_order_ids': list(self.settled_leg_order_ids),
|
|
1201
|
+
'expected_leg_count': self.expected_leg_count,
|
|
1202
|
+
'booked_leg_count': self.booked_leg_count,
|
|
1203
|
+
'dropped_close_qty': self.dropped_close_qty,
|
|
1204
|
+
'dropped_fill_slices': [
|
|
1205
|
+
[fill_id, qty] for fill_id, qty in self.dropped_fill_slices
|
|
1206
|
+
],
|
|
1059
1207
|
}
|
|
1060
1208
|
|
|
1061
1209
|
@classmethod
|
|
@@ -1162,6 +1310,118 @@ class PendingDefensiveClose:
|
|
|
1162
1310
|
f"PendingDefensiveClose.unapplied_partial_qty must be "
|
|
1163
1311
|
f"non-negative: {data!r}"
|
|
1164
1312
|
)
|
|
1313
|
+
expected_close_qty_raw = data.get('expected_close_qty')
|
|
1314
|
+
if expected_close_qty_raw is None:
|
|
1315
|
+
expected_close_qty: float | None = None
|
|
1316
|
+
elif isinstance(expected_close_qty_raw, (int, float)):
|
|
1317
|
+
expected_close_qty = float(expected_close_qty_raw)
|
|
1318
|
+
if expected_close_qty <= 0.0:
|
|
1319
|
+
raise ValueError(
|
|
1320
|
+
f"PendingDefensiveClose.expected_close_qty must be "
|
|
1321
|
+
f"positive or None: {data!r}"
|
|
1322
|
+
)
|
|
1323
|
+
else:
|
|
1324
|
+
raise ValueError(
|
|
1325
|
+
f"PendingDefensiveClose.expected_close_qty must be "
|
|
1326
|
+
f"numeric or None: {data!r}"
|
|
1327
|
+
)
|
|
1328
|
+
applied_close_qty_raw = data.get('applied_close_qty', 0.0)
|
|
1329
|
+
if not isinstance(applied_close_qty_raw, (int, float)):
|
|
1330
|
+
raise ValueError(
|
|
1331
|
+
f"PendingDefensiveClose.applied_close_qty must be "
|
|
1332
|
+
f"numeric: {data!r}"
|
|
1333
|
+
)
|
|
1334
|
+
applied_close_qty = float(applied_close_qty_raw)
|
|
1335
|
+
if applied_close_qty < 0.0:
|
|
1336
|
+
raise ValueError(
|
|
1337
|
+
f"PendingDefensiveClose.applied_close_qty must be "
|
|
1338
|
+
f"non-negative: {data!r}"
|
|
1339
|
+
)
|
|
1340
|
+
settled_leg_order_ids_raw = data.get('settled_leg_order_ids', [])
|
|
1341
|
+
if not isinstance(settled_leg_order_ids_raw, (list, tuple)):
|
|
1342
|
+
raise ValueError(
|
|
1343
|
+
f"PendingDefensiveClose.settled_leg_order_ids must be "
|
|
1344
|
+
f"a list or tuple: {data!r}"
|
|
1345
|
+
)
|
|
1346
|
+
settled_leg_order_ids_list: list[str] = []
|
|
1347
|
+
for leg_order_id in settled_leg_order_ids_raw:
|
|
1348
|
+
if not isinstance(leg_order_id, str):
|
|
1349
|
+
raise ValueError(
|
|
1350
|
+
f"PendingDefensiveClose.settled_leg_order_ids items "
|
|
1351
|
+
f"must be strings: {data!r}"
|
|
1352
|
+
)
|
|
1353
|
+
settled_leg_order_ids_list.append(leg_order_id)
|
|
1354
|
+
expected_leg_count_raw = data.get('expected_leg_count', 0)
|
|
1355
|
+
if isinstance(expected_leg_count_raw, bool) or not isinstance(
|
|
1356
|
+
expected_leg_count_raw, int,
|
|
1357
|
+
):
|
|
1358
|
+
raise ValueError(
|
|
1359
|
+
f"PendingDefensiveClose.expected_leg_count must be "
|
|
1360
|
+
f"an int: {data!r}"
|
|
1361
|
+
)
|
|
1362
|
+
if expected_leg_count_raw < 0:
|
|
1363
|
+
raise ValueError(
|
|
1364
|
+
f"PendingDefensiveClose.expected_leg_count must be "
|
|
1365
|
+
f"non-negative: {data!r}"
|
|
1366
|
+
)
|
|
1367
|
+
booked_leg_count_raw = data.get('booked_leg_count', 0)
|
|
1368
|
+
if isinstance(booked_leg_count_raw, bool) or not isinstance(
|
|
1369
|
+
booked_leg_count_raw, int,
|
|
1370
|
+
):
|
|
1371
|
+
raise ValueError(
|
|
1372
|
+
f"PendingDefensiveClose.booked_leg_count must be "
|
|
1373
|
+
f"an int: {data!r}"
|
|
1374
|
+
)
|
|
1375
|
+
if booked_leg_count_raw < 0:
|
|
1376
|
+
raise ValueError(
|
|
1377
|
+
f"PendingDefensiveClose.booked_leg_count must be "
|
|
1378
|
+
f"non-negative: {data!r}"
|
|
1379
|
+
)
|
|
1380
|
+
dropped_close_qty_raw = data.get('dropped_close_qty', 0.0)
|
|
1381
|
+
if not isinstance(dropped_close_qty_raw, (int, float)):
|
|
1382
|
+
raise ValueError(
|
|
1383
|
+
f"PendingDefensiveClose.dropped_close_qty must be "
|
|
1384
|
+
f"numeric: {data!r}"
|
|
1385
|
+
)
|
|
1386
|
+
dropped_close_qty = float(dropped_close_qty_raw)
|
|
1387
|
+
if dropped_close_qty < 0.0:
|
|
1388
|
+
raise ValueError(
|
|
1389
|
+
f"PendingDefensiveClose.dropped_close_qty must be "
|
|
1390
|
+
f"non-negative: {data!r}"
|
|
1391
|
+
)
|
|
1392
|
+
dropped_fill_slices_raw = data.get('dropped_fill_slices', [])
|
|
1393
|
+
if not isinstance(dropped_fill_slices_raw, (list, tuple)):
|
|
1394
|
+
raise ValueError(
|
|
1395
|
+
f"PendingDefensiveClose.dropped_fill_slices must be "
|
|
1396
|
+
f"a list or tuple: {data!r}"
|
|
1397
|
+
)
|
|
1398
|
+
dropped_fill_slices_list: list[tuple[str, float]] = []
|
|
1399
|
+
for slice_payload in dropped_fill_slices_raw:
|
|
1400
|
+
if (not isinstance(slice_payload, (list, tuple))
|
|
1401
|
+
or len(slice_payload) != 2):
|
|
1402
|
+
raise ValueError(
|
|
1403
|
+
f"PendingDefensiveClose.dropped_fill_slices items must "
|
|
1404
|
+
f"be (fill_id, qty) pairs: {data!r}"
|
|
1405
|
+
)
|
|
1406
|
+
slice_fill_id, slice_qty = slice_payload
|
|
1407
|
+
if not isinstance(slice_fill_id, str):
|
|
1408
|
+
raise ValueError(
|
|
1409
|
+
f"PendingDefensiveClose.dropped_fill_slices fill ids "
|
|
1410
|
+
f"must be strings: {data!r}"
|
|
1411
|
+
)
|
|
1412
|
+
if isinstance(slice_qty, bool) or not isinstance(
|
|
1413
|
+
slice_qty, (int, float),
|
|
1414
|
+
):
|
|
1415
|
+
raise ValueError(
|
|
1416
|
+
f"PendingDefensiveClose.dropped_fill_slices quantities "
|
|
1417
|
+
f"must be numeric: {data!r}"
|
|
1418
|
+
)
|
|
1419
|
+
if slice_qty < 0.0:
|
|
1420
|
+
raise ValueError(
|
|
1421
|
+
f"PendingDefensiveClose.dropped_fill_slices quantities "
|
|
1422
|
+
f"must be non-negative: {data!r}"
|
|
1423
|
+
)
|
|
1424
|
+
dropped_fill_slices_list.append((slice_fill_id, float(slice_qty)))
|
|
1165
1425
|
return cls(
|
|
1166
1426
|
entry_id=entry_id,
|
|
1167
1427
|
close_intent_key=close_intent_key,
|
|
@@ -1176,6 +1436,13 @@ class PendingDefensiveClose:
|
|
|
1176
1436
|
pre_close_position_size=pre_close_position_size,
|
|
1177
1437
|
fifo_closed_entry_ids=tuple(fifo_closed_entry_ids_list),
|
|
1178
1438
|
unapplied_partial_qty=unapplied_partial_qty,
|
|
1439
|
+
expected_close_qty=expected_close_qty,
|
|
1440
|
+
applied_close_qty=applied_close_qty,
|
|
1441
|
+
settled_leg_order_ids=tuple(settled_leg_order_ids_list),
|
|
1442
|
+
expected_leg_count=expected_leg_count_raw,
|
|
1443
|
+
booked_leg_count=booked_leg_count_raw,
|
|
1444
|
+
dropped_close_qty=dropped_close_qty,
|
|
1445
|
+
dropped_fill_slices=tuple(dropped_fill_slices_list),
|
|
1179
1446
|
)
|
|
1180
1447
|
|
|
1181
1448
|
|
|
@@ -66,6 +66,7 @@ from pynecore.core.broker.store_helpers import (
|
|
|
66
66
|
BRACKET_OWN_STATE_CLEARING,
|
|
67
67
|
BRACKET_OWN_STATE_RELEASED,
|
|
68
68
|
CLOSE_LEG_STATE_DISPATCHED,
|
|
69
|
+
ENTRY_KIND_POSITION,
|
|
69
70
|
EXTRAS_KEY_BRACKET_OWN_ATTACH_COID,
|
|
70
71
|
EXTRAS_KEY_BRACKET_OWN_CLEAR_COID,
|
|
71
72
|
EXTRAS_KEY_BRACKET_OWN_LEG_ID,
|
|
@@ -111,7 +112,15 @@ class CloseFanResult:
|
|
|
111
112
|
"""Outcome of one fanned-out close, for the caller's return shaping / audit.
|
|
112
113
|
|
|
113
114
|
:ivar legs: ``(leg_id, broker-grid volume)`` pairs actually dispatched, in
|
|
114
|
-
FIFO order. Empty when nothing was open or the close was skipped.
|
|
115
|
+
FIFO order. Empty when nothing was open or the close was skipped. The
|
|
116
|
+
volumes are BROKER-grid integers (cTrader centi-units, Capital.com
|
|
117
|
+
lot-step counts, ...) — never comparable with a Pine-unit fill
|
|
118
|
+
quantity; use ``dispatched_qty`` for that.
|
|
119
|
+
:ivar dispatched_qty: Pine-unit quantity the dispatched legs cover — the
|
|
120
|
+
summed FIFO plan slices, in the SAME unit as
|
|
121
|
+
:attr:`~pynecore.core.broker.models.OrderEvent.fill_qty`, so the
|
|
122
|
+
caller can compare it with the fills that come back. ``0.0`` when
|
|
123
|
+
nothing was dispatched.
|
|
115
124
|
:ivar shortfall: Pine-unit quantity the open legs could not cover (the
|
|
116
125
|
broker holds less than Pine believes). ``0.0`` in the normal case; a
|
|
117
126
|
positive value is the caller's signal to log, not halt.
|
|
@@ -121,6 +130,7 @@ class CloseFanResult:
|
|
|
121
130
|
``skipped=False``, which means the symbol was already flat.
|
|
122
131
|
"""
|
|
123
132
|
legs: tuple[tuple[str, int], ...]
|
|
133
|
+
dispatched_qty: float
|
|
124
134
|
shortfall: float
|
|
125
135
|
skipped: bool
|
|
126
136
|
|
|
@@ -146,11 +156,16 @@ class BracketFanResult:
|
|
|
146
156
|
"""Outcome of one exit-bracket replication across a position's legs.
|
|
147
157
|
|
|
148
158
|
:ivar legs: Broker leg ids the bracket was replicated onto, in FIFO order.
|
|
149
|
-
:ivar skipped: ``True`` when the symbol was flat (no
|
|
150
|
-
|
|
159
|
+
:ivar skipped: ``True`` when nothing was amended — the symbol was flat (no
|
|
160
|
+
position-side legs to protect) or the position book is incomplete (a
|
|
161
|
+
protected-side entry's fill is not yet journaled) — a non-halting
|
|
162
|
+
no-op; the caller re-evaluates next bar.
|
|
163
|
+
:ivar skip_reason: Human-readable cause for an incomplete-book skip, for
|
|
164
|
+
the caller's skip message; ``None`` for the flat / flipped-side skips.
|
|
151
165
|
"""
|
|
152
166
|
legs: tuple[str, ...]
|
|
153
167
|
skipped: bool
|
|
168
|
+
skip_reason: str | None = None
|
|
154
169
|
|
|
155
170
|
|
|
156
171
|
@dataclass(frozen=True)
|
|
@@ -218,13 +233,18 @@ class OneWayEmulator:
|
|
|
218
233
|
pos = aggregate_positions(intent.symbol, legs)
|
|
219
234
|
if pos is None or pos.side == 'flat':
|
|
220
235
|
# Nothing open, or offsetting legs net to flat — benign no-op.
|
|
221
|
-
return CloseFanResult(
|
|
236
|
+
return CloseFanResult(
|
|
237
|
+
legs=(), dispatched_qty=0.0, shortfall=0.0, skipped=False,
|
|
238
|
+
)
|
|
222
239
|
# CloseIntent.side is the close direction ("sell" closes a long); the
|
|
223
240
|
# legs to reduce sit on the opposite side of the book.
|
|
224
241
|
leg_side = 'buy' if intent.side == 'sell' else 'sell'
|
|
225
242
|
closes, shortfall = select_legs_for_close(intent.qty, legs, leg_side)
|
|
226
243
|
if not closes:
|
|
227
|
-
return CloseFanResult(
|
|
244
|
+
return CloseFanResult(
|
|
245
|
+
legs=(), dispatched_qty=0.0, shortfall=shortfall,
|
|
246
|
+
skipped=False,
|
|
247
|
+
)
|
|
228
248
|
await self._reject_unsupported_partial_closes(
|
|
229
249
|
closes, legs, symbol=intent.symbol,
|
|
230
250
|
intent_key=intent.intent_key, port=port,
|
|
@@ -237,9 +257,16 @@ class OneWayEmulator:
|
|
|
237
257
|
)
|
|
238
258
|
if not dispatched:
|
|
239
259
|
# Every slice rounded below the broker grid — skip, do not halt.
|
|
240
|
-
return CloseFanResult(
|
|
260
|
+
return CloseFanResult(
|
|
261
|
+
legs=(), dispatched_qty=0.0, shortfall=shortfall, skipped=True,
|
|
262
|
+
)
|
|
241
263
|
return CloseFanResult(
|
|
242
|
-
legs=tuple(dispatched),
|
|
264
|
+
legs=tuple(dispatched),
|
|
265
|
+
# Pine units, NOT the broker-grid volumes above: the caller
|
|
266
|
+
# compares this with the fill quantities the legs report back,
|
|
267
|
+
# and those arrive in Pine units on every venue.
|
|
268
|
+
dispatched_qty=round(sum(close.qty for close in closes), 12),
|
|
269
|
+
shortfall=shortfall, skipped=False,
|
|
243
270
|
)
|
|
244
271
|
|
|
245
272
|
async def run_reversal(
|
|
@@ -474,7 +501,8 @@ class OneWayEmulator:
|
|
|
474
501
|
in the ownership index (keyed by the exit's ``intent_key``) BEFORE its
|
|
475
502
|
amend, so :meth:`run_exit_bracket_clear` later clears ONLY the legs this
|
|
476
503
|
exit owns and :meth:`restart_replay` can re-assert or release them. A flat
|
|
477
|
-
symbol
|
|
504
|
+
symbol — or a book whose net side is the opposite of the one this exit
|
|
505
|
+
protects — is a non-halting skip.
|
|
478
506
|
|
|
479
507
|
Re-running it (a modify, or a re-attach after pyramiding) upserts the
|
|
480
508
|
same per-leg rows idempotently: the row key is STABLE per (exit
|
|
@@ -485,6 +513,11 @@ class OneWayEmulator:
|
|
|
485
513
|
leg (the dispatch coid alone would collide — it encodes pine_id but not
|
|
486
514
|
from_entry). Legs that have since closed are released by the restart /
|
|
487
515
|
reconcile pass, not here.
|
|
516
|
+
|
|
517
|
+
The fan is also deferred (``skipped=True`` with a ``skip_reason``)
|
|
518
|
+
while any protected-side position entry's fill is still pending in the
|
|
519
|
+
journal — see :meth:`_pending_position_entry` — so it always runs over
|
|
520
|
+
a complete book and covers a same-sync pyramid entry's own leg.
|
|
488
521
|
"""
|
|
489
522
|
intent = envelope.intent
|
|
490
523
|
assert isinstance(intent, ExitIntent)
|
|
@@ -504,8 +537,43 @@ class OneWayEmulator:
|
|
|
504
537
|
)
|
|
505
538
|
legs = await port.fetch_raw_positions(intent.symbol)
|
|
506
539
|
side, on_side = net_survivor_legs(legs)
|
|
507
|
-
|
|
540
|
+
# The net side must be the one this exit protects: ``intent.side`` is
|
|
541
|
+
# the CLOSING direction, so a sell-side exit belongs to a long. When
|
|
542
|
+
# the book has flipped since the exit was placed — the parent entry was
|
|
543
|
+
# reversed away and the stale exit is re-dispatched against the new
|
|
544
|
+
# opposite position — the levels are upside down for that side (a
|
|
545
|
+
# long's take-profit sits ABOVE the price, a short's must sit below).
|
|
546
|
+
# Venues reject that outright, which escalates into a defensive close
|
|
547
|
+
# for a parent that no longer exists. The position this exit protects
|
|
548
|
+
# is simply not there, so it is the same non-halting skip as a flat
|
|
549
|
+
# book: no bracket, re-evaluated on the next bar.
|
|
550
|
+
protected_side = 'buy' if intent.side == 'sell' else 'sell'
|
|
551
|
+
if side == 'flat' or not on_side or side != protected_side:
|
|
508
552
|
return BracketFanResult(legs=(), skipped=True)
|
|
553
|
+
# Incomplete-book gate: a protected-side position entry whose FILL is
|
|
554
|
+
# not yet journaled means the snapshot above cannot be tied to that
|
|
555
|
+
# entry's venue leg yet (the leg id is only learned from the fill, and
|
|
556
|
+
# on a same-sync pyramid the fill event has not been drained when this
|
|
557
|
+
# fan runs). Fanning now would attach the bracket onto the OLDER legs
|
|
558
|
+
# only, the intent would go active, and nothing ever re-fans — the
|
|
559
|
+
# newest leg of a pyramid round would stay permanently unprotected. A
|
|
560
|
+
# mixed book is worse: netting computed over the incomplete snapshot
|
|
561
|
+
# can mis-drop a true survivor and the clear below would strip its live
|
|
562
|
+
# bracket. Defer the WHOLE fan — the same non-halting skip as a flat
|
|
563
|
+
# book — until every position-opening dispatch on this side has its
|
|
564
|
+
# fill on the books; the re-run then covers all legs.
|
|
565
|
+
if self._store_ctx is not None:
|
|
566
|
+
pending_coid = self._pending_position_entry(
|
|
567
|
+
intent.symbol, protected_side,
|
|
568
|
+
)
|
|
569
|
+
if pending_coid is not None:
|
|
570
|
+
return BracketFanResult(
|
|
571
|
+
legs=(), skipped=True,
|
|
572
|
+
skip_reason=(
|
|
573
|
+
f"position book incomplete — entry dispatch "
|
|
574
|
+
f"{pending_coid!r} has not journaled its fill yet"
|
|
575
|
+
),
|
|
576
|
+
)
|
|
509
577
|
survivors = {leg.leg_id for leg in on_side}
|
|
510
578
|
# A re-attach can narrow the survivor set: a manual hedge or an
|
|
511
579
|
# interrupted reversal opens an opposing leg that virtually FIFO-consumes
|
|
@@ -591,6 +659,32 @@ class OneWayEmulator:
|
|
|
591
659
|
replicated.append(leg.leg_id)
|
|
592
660
|
return BracketFanResult(legs=tuple(replicated), skipped=False)
|
|
593
661
|
|
|
662
|
+
def _pending_position_entry(self, symbol: str, protected_side: str) -> str | None:
|
|
663
|
+
"""Client-order-id of a protected-side position entry awaiting its fill.
|
|
664
|
+
|
|
665
|
+
A live journal row with ``extras['kind'] == ENTRY_KIND_POSITION`` opens
|
|
666
|
+
a venue leg immediately (MARKET dispatch or an adopted position), so
|
|
667
|
+
while its cumulative ``filled_qty`` is short of ``qty`` the book
|
|
668
|
+
snapshot is incomplete for ``protected_side``. Working (LIMIT / STOP)
|
|
669
|
+
rows are excluded — they hold no leg until their fill promotes them to
|
|
670
|
+
``ENTRY_KIND_POSITION`` — and so are entries opening the opposite side.
|
|
671
|
+
|
|
672
|
+
:param symbol: The exit's canonical broker symbol.
|
|
673
|
+
:param protected_side: Position side the exit protects (``'buy'`` /
|
|
674
|
+
``'sell'``).
|
|
675
|
+
:return: The first pending row's ``client_order_id``, or ``None`` when
|
|
676
|
+
every protected-side position entry has its fill on the books.
|
|
677
|
+
"""
|
|
678
|
+
assert self._store_ctx is not None
|
|
679
|
+
for row in self._store_ctx.iter_live_orders(symbol=symbol):
|
|
680
|
+
if (row.extras or {}).get('kind') != ENTRY_KIND_POSITION:
|
|
681
|
+
continue
|
|
682
|
+
if row.side != protected_side:
|
|
683
|
+
continue
|
|
684
|
+
if row.filled_qty < row.qty - 1e-9:
|
|
685
|
+
return row.client_order_id
|
|
686
|
+
return None
|
|
687
|
+
|
|
594
688
|
async def _clear_dropped_survivor_legs(
|
|
595
689
|
self, envelope: 'DispatchEnvelope', intent: ExitIntent, *,
|
|
596
690
|
survivors: set[str], port: 'PositionPort',
|