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.
Files changed (270) hide show
  1. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/PKG-INFO +12 -5
  2. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/README.md +11 -4
  3. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/pyproject.toml +1 -1
  4. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/cli/commands/run.py +6 -2
  5. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/intent_builder.py +6 -0
  6. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/models.py +271 -4
  7. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/one_way_emulator.py +103 -9
  8. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/store_helpers.py +115 -1
  9. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/sync_engine.py +1628 -79
  10. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/datetime.py +47 -0
  11. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/import_hook.py +2 -2
  12. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/instance_state.py +150 -2
  13. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/live_runner.py +371 -35
  14. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/plugin/live_provider.py +49 -0
  15. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/script_runner.py +323 -60
  16. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/security.py +23 -7
  17. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/__init__.py +450 -66
  18. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/_timeframe_change.py +6 -0
  19. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/chart.py +3 -3
  20. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/linefill.py +12 -0
  21. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/math.py +30 -13
  22. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/polyline.py +2 -2
  23. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/session.py +32 -26
  24. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/strategy/__init__.py +971 -295
  25. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/strategy/opentrades.py +1 -1
  26. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/ta.py +123 -59
  27. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/table.py +1 -1
  28. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/function_isolation.py +24 -3
  29. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/module_properties.json +15 -9
  30. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/strategy.py +15 -0
  31. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/LICENSE +0 -0
  32. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/MANIFEST.in +0 -0
  33. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/NOTICE +0 -0
  34. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/build/opencode_pyneruntime.egg-info/SOURCES.txt +0 -0
  35. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/setup.cfg +0 -0
  36. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/__init__.py +0 -0
  37. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/cli/__init__.py +0 -0
  38. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/cli/app.py +0 -0
  39. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/cli/commands/__init__.py +0 -0
  40. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/cli/commands/benchmark.py +0 -0
  41. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/cli/commands/compile.py +0 -0
  42. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/cli/commands/data.py +0 -0
  43. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/cli/commands/debug.py +0 -0
  44. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/cli/commands/optimize.py +0 -0
  45. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/cli/commands/plugin.py +0 -0
  46. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/cli/pluggable.py +0 -0
  47. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/cli/utils/__init__.py +0 -0
  48. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/cli/utils/api_error_handler.py +0 -0
  49. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/cli/utils/broker_picker.py +0 -0
  50. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/cli/utils/error_hook.py +0 -0
  51. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/cli/utils/keyreader.py +0 -0
  52. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/cli/utils/provider_picker.py +0 -0
  53. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/cli/utils/symbol_browser.py +0 -0
  54. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/__init__.py +0 -0
  55. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/_file_io.py +0 -0
  56. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/aggregator.py +0 -0
  57. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/bar_magnifier.py +0 -0
  58. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/__init__.py +0 -0
  59. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/defaults.py +0 -0
  60. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/disappearance.py +0 -0
  61. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/emulator.py +0 -0
  62. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/exceptions.py +0 -0
  63. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/idempotency.py +0 -0
  64. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/journal.py +0 -0
  65. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/native_failsafe_manager.py +0 -0
  66. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/position.py +0 -0
  67. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/run_identity.py +0 -0
  68. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/software_entry_stop_engine.py +0 -0
  69. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/software_partial_bracket_engine.py +0 -0
  70. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/spot_inventory.py +0 -0
  71. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/storage.py +0 -0
  72. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/broker/validation.py +0 -0
  73. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/class_property.py +0 -0
  74. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/config.py +0 -0
  75. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/csv_file.py +0 -0
  76. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/currency.py +0 -0
  77. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/data_converter.py +0 -0
  78. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/download_info.py +0 -0
  79. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/download_runner.py +0 -0
  80. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/drawing_snapshot.py +0 -0
  81. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/htf_aggregator.py +0 -0
  82. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/live_ltf_collector.py +0 -0
  83. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/live_ltf_window.py +0 -0
  84. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/module_property.py +0 -0
  85. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/ohlcv.py +0 -0
  86. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/ohlcv_importers.py +0 -0
  87. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/ohlcv_legacy.py +0 -0
  88. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/overload.py +0 -0
  89. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/pine_cast.py +0 -0
  90. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/pine_compare.py +0 -0
  91. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/pine_export.py +0 -0
  92. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/pine_method.py +0 -0
  93. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/pine_range.py +0 -0
  94. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/pine_udt.py +0 -0
  95. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/plugin/__init__.py +0 -0
  96. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/plugin/broker.py +0 -0
  97. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/plugin/cli.py +0 -0
  98. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/plugin/provider.py +0 -0
  99. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/provider_string.py +0 -0
  100. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/random.py +0 -0
  101. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/resampler.py +0 -0
  102. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/rolling_sum.py +0 -0
  103. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/safe_convert.py +0 -0
  104. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/script.py +0 -0
  105. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/security_process.py +0 -0
  106. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/security_shm.py +0 -0
  107. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/series.py +0 -0
  108. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/session.py +0 -0
  109. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/strategy_stats.py +0 -0
  110. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/symbol_map.py +0 -0
  111. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/syminfo.py +0 -0
  112. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/core/viz.py +0 -0
  113. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/_fixnan.py +0 -0
  114. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/_math_stateful.py +0 -0
  115. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/adjustment.py +0 -0
  116. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/alert.py +0 -0
  117. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/alert.pyi +0 -0
  118. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/array.py +0 -0
  119. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/barmerge.py +0 -0
  120. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/barstate.py +0 -0
  121. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/box.py +0 -0
  122. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/color.py +0 -0
  123. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/color.pyi +0 -0
  124. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/currency.py +0 -0
  125. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/dayofweek.py +0 -0
  126. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/dayofweek.pyi +0 -0
  127. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/display.py +0 -0
  128. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/dividends.py +0 -0
  129. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/earnings.py +0 -0
  130. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/extend.py +0 -0
  131. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/font.py +0 -0
  132. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/footprint.py +0 -0
  133. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/format.py +0 -0
  134. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/hline.py +0 -0
  135. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/hline.pyi +0 -0
  136. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/label.py +0 -0
  137. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/line.py +0 -0
  138. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/location.py +0 -0
  139. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/log.py +0 -0
  140. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/map.py +0 -0
  141. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/matrix.py +0 -0
  142. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/order.py +0 -0
  143. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/pivotpointtype.py +0 -0
  144. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/plot.py +0 -0
  145. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/plot.pyi +0 -0
  146. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/position.py +0 -0
  147. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/request.py +0 -0
  148. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/runtime.py +0 -0
  149. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/scale.py +0 -0
  150. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/session.pyi +0 -0
  151. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/shape.py +0 -0
  152. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/size.py +0 -0
  153. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/splits.py +0 -0
  154. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/strategy/closedtrades.py +0 -0
  155. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/strategy/closedtrades.pyi +0 -0
  156. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/strategy/commission.py +0 -0
  157. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/strategy/direction.py +0 -0
  158. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/strategy/oca.py +0 -0
  159. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/strategy/opentrades.pyi +0 -0
  160. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/strategy/risk.py +0 -0
  161. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/string.py +0 -0
  162. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/syminfo.py +0 -0
  163. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/text.py +0 -0
  164. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/ticker.py +0 -0
  165. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/timeframe.py +0 -0
  166. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/volume_row.py +0 -0
  167. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/xloc.py +0 -0
  168. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/lib/yloc.py +0 -0
  169. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/providers/__init__.py +0 -0
  170. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/providers/ccxt.py +0 -0
  171. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/providers/replay.py +0 -0
  172. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/pynesys/__init__.py +0 -0
  173. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/pynesys/api.py +0 -0
  174. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/pynesys/compiler.py +0 -0
  175. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/standalone.py +0 -0
  176. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/testing/__init__.py +0 -0
  177. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/testing/broker_lab/__init__.py +0 -0
  178. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/testing/broker_lab/__main__.py +0 -0
  179. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/testing/broker_lab/cli.py +0 -0
  180. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/testing/broker_lab/generate.py +0 -0
  181. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/testing/broker_lab/model.py +0 -0
  182. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/testing/broker_lab/reference.py +0 -0
  183. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/testing/broker_lab/runner.py +0 -0
  184. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/testing/broker_lab/scheduler.py +0 -0
  185. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/testing/broker_lab/subprocess.py +0 -0
  186. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/__init__.py +0 -0
  187. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/builtin_shadow.py +0 -0
  188. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/closure_arguments_transformer.py +0 -0
  189. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/display_rewrite.py +0 -0
  190. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/dynamic_default.py +0 -0
  191. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/float_tolerance.py +0 -0
  192. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/import_lifter.py +0 -0
  193. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/import_normalizer.py +0 -0
  194. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/inline_series_hoist.py +0 -0
  195. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/input_transformer.py +0 -0
  196. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/lib_series.py +0 -0
  197. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/locations.py +0 -0
  198. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/module_property.py +0 -0
  199. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/persistent.py +0 -0
  200. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/persistent_series.py +0 -0
  201. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/pine_truthiness.py +0 -0
  202. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/safe_convert_transformer.py +0 -0
  203. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/safe_division_transformer.py +0 -0
  204. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/script_requirements.py +0 -0
  205. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/security.py +0 -0
  206. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/security_instantiation.py +0 -0
  207. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/series.py +0 -0
  208. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/slot_layout.py +0 -0
  209. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/type_checking_stripper.py +0 -0
  210. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/transformers/unused_series_detector.py +0 -0
  211. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/__init__.py +0 -0
  212. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/alert.py +0 -0
  213. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/barmerge.py +0 -0
  214. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/base.py +0 -0
  215. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/box.py +0 -0
  216. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/chart.py +0 -0
  217. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/color.py +0 -0
  218. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/currency.py +0 -0
  219. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/datetime.py +0 -0
  220. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/display.py +0 -0
  221. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/dividends.py +0 -0
  222. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/earnings.py +0 -0
  223. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/extend.py +0 -0
  224. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/font.py +0 -0
  225. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/footprint.py +0 -0
  226. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/format.py +0 -0
  227. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/hline.py +0 -0
  228. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/ib_persistent.py +0 -0
  229. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/ib_persistent.pyi +0 -0
  230. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/label.py +0 -0
  231. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/line.py +0 -0
  232. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/linefill.py +0 -0
  233. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/location.py +0 -0
  234. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/matrix.py +0 -0
  235. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/na.py +0 -0
  236. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/na.pyi +0 -0
  237. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/ohlcv.py +0 -0
  238. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/order.py +0 -0
  239. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/persistent.py +0 -0
  240. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/persistent.pyi +0 -0
  241. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/pine_types.py +0 -0
  242. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/pine_types.pyi +0 -0
  243. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/pivotpointtype.py +0 -0
  244. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/plot.py +0 -0
  245. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/plot_meta.py +0 -0
  246. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/polyline.py +0 -0
  247. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/position.py +0 -0
  248. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/scale.py +0 -0
  249. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/script_type.py +0 -0
  250. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/series.py +0 -0
  251. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/series.pyi +0 -0
  252. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/session.py +0 -0
  253. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/shape.py +0 -0
  254. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/size.py +0 -0
  255. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/source.py +0 -0
  256. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/splits.py +0 -0
  257. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/table.py +0 -0
  258. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/text.py +0 -0
  259. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/type_checker.py +0 -0
  260. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/type_checker.pyi +0 -0
  261. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/volume_row.py +0 -0
  262. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/weekdays.py +0 -0
  263. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/xloc.py +0 -0
  264. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/types/yloc.py +0 -0
  265. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/utils/__init__.py +0 -0
  266. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/utils/file_utils.py +0 -0
  267. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/utils/rich/__init__.py +0 -0
  268. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/utils/rich/date_column.py +0 -0
  269. {opencode_pyneruntime-6.8.5 → opencode_pyneruntime-6.8.7}/src/pynecore/utils/sequence_view.py +0 -0
  270. {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.5
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
- pyne run my_strategy.py dnse:VN30F1M@5 --from -500
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
- Unit tests: `pytest plugins/dnse/tests/`. The vendored SDK, a mirror of DNSE's
235
- OpenAPI docs (`docs/dnse-openapi-documentation/`) with a sync/verify tool, and the
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
- pyne run my_strategy.py dnse:VN30F1M@5 --from -500
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
- Unit tests: `pytest plugins/dnse/tests/`. The vendored SDK, a mirror of DNSE's
190
- OpenAPI docs (`docs/dnse-openapi-documentation/`) with a sync/verify tool, and the
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.5" # PineVersion.Major.Minor — tracks upstream
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
- suffix = (" [mapping present]" if r.has_security_mapping
1009
- else f" [needs --security '{r.timeframe}=<base file>' in backtest]")
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:
@@ -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
 
@@ -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 ``partial`` events that the engine could NOT
967
- apply to :attr:`OrderSyncEngine._position.size` because the
968
- parent ENTRY fill had not yet arrived (``_position.size ==
969
- 0.0``). Tracked separately from :attr:`partial_filled_qty` so
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 position-side legs to
150
- protect) a non-halting no-op; the caller re-evaluates next bar.
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(legs=(), shortfall=0.0, skipped=False)
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(legs=(), shortfall=shortfall, skipped=False)
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(legs=(), shortfall=shortfall, skipped=True)
260
+ return CloseFanResult(
261
+ legs=(), dispatched_qty=0.0, shortfall=shortfall, skipped=True,
262
+ )
241
263
  return CloseFanResult(
242
- legs=tuple(dispatched), shortfall=shortfall, skipped=False,
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 is a non-halting skip.
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
- if side == 'flat' or not on_side:
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',