opencode-pyneruntime 6.6.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. opencode_pyneruntime-6.6.4.dist-info/METADATA +281 -0
  2. opencode_pyneruntime-6.6.4.dist-info/RECORD +261 -0
  3. opencode_pyneruntime-6.6.4.dist-info/WHEEL +5 -0
  4. opencode_pyneruntime-6.6.4.dist-info/entry_points.txt +6 -0
  5. opencode_pyneruntime-6.6.4.dist-info/licenses/LICENSE +201 -0
  6. opencode_pyneruntime-6.6.4.dist-info/licenses/NOTICE +21 -0
  7. opencode_pyneruntime-6.6.4.dist-info/top_level.txt +1 -0
  8. pynecore/__init__.py +6 -0
  9. pynecore/cli/__init__.py +2 -0
  10. pynecore/cli/app.py +238 -0
  11. pynecore/cli/commands/__init__.py +343 -0
  12. pynecore/cli/commands/benchmark.py +186 -0
  13. pynecore/cli/commands/compile.py +198 -0
  14. pynecore/cli/commands/data.py +857 -0
  15. pynecore/cli/commands/debug.py +63 -0
  16. pynecore/cli/commands/optimize.py +956 -0
  17. pynecore/cli/commands/plugin.py +242 -0
  18. pynecore/cli/commands/run.py +2006 -0
  19. pynecore/cli/pluggable.py +132 -0
  20. pynecore/cli/utils/__init__.py +0 -0
  21. pynecore/cli/utils/api_error_handler.py +168 -0
  22. pynecore/cli/utils/broker_picker.py +330 -0
  23. pynecore/cli/utils/error_hook.py +28 -0
  24. pynecore/cli/utils/keyreader.py +178 -0
  25. pynecore/cli/utils/provider_picker.py +19 -0
  26. pynecore/cli/utils/symbol_browser.py +1149 -0
  27. pynecore/core/__init__.py +0 -0
  28. pynecore/core/aggregator.py +257 -0
  29. pynecore/core/bar_magnifier.py +168 -0
  30. pynecore/core/broker/__init__.py +64 -0
  31. pynecore/core/broker/defaults.py +113 -0
  32. pynecore/core/broker/disappearance.py +927 -0
  33. pynecore/core/broker/emulator.py +345 -0
  34. pynecore/core/broker/exceptions.py +346 -0
  35. pynecore/core/broker/idempotency.py +401 -0
  36. pynecore/core/broker/intent_builder.py +334 -0
  37. pynecore/core/broker/journal.py +1785 -0
  38. pynecore/core/broker/models.py +1600 -0
  39. pynecore/core/broker/native_failsafe_manager.py +1436 -0
  40. pynecore/core/broker/one_way_emulator.py +1128 -0
  41. pynecore/core/broker/position.py +787 -0
  42. pynecore/core/broker/run_identity.py +126 -0
  43. pynecore/core/broker/software_entry_stop_engine.py +351 -0
  44. pynecore/core/broker/software_partial_bracket_engine.py +1379 -0
  45. pynecore/core/broker/spot_inventory.py +1327 -0
  46. pynecore/core/broker/storage.py +2655 -0
  47. pynecore/core/broker/store_helpers.py +2161 -0
  48. pynecore/core/broker/sync_engine.py +16070 -0
  49. pynecore/core/broker/validation.py +382 -0
  50. pynecore/core/class_property.py +7 -0
  51. pynecore/core/config.py +392 -0
  52. pynecore/core/csv_file.py +547 -0
  53. pynecore/core/currency.py +262 -0
  54. pynecore/core/data_converter.py +1002 -0
  55. pynecore/core/datetime.py +296 -0
  56. pynecore/core/download_info.py +71 -0
  57. pynecore/core/download_runner.py +274 -0
  58. pynecore/core/htf_aggregator.py +181 -0
  59. pynecore/core/import_hook.py +358 -0
  60. pynecore/core/instance_state.py +494 -0
  61. pynecore/core/live_ltf_collector.py +442 -0
  62. pynecore/core/live_ltf_window.py +189 -0
  63. pynecore/core/live_runner.py +1347 -0
  64. pynecore/core/module_property.py +26 -0
  65. pynecore/core/ohlcv_file.py +1888 -0
  66. pynecore/core/overload.py +371 -0
  67. pynecore/core/pine_cast.py +113 -0
  68. pynecore/core/pine_export.py +95 -0
  69. pynecore/core/pine_method.py +244 -0
  70. pynecore/core/pine_range.py +86 -0
  71. pynecore/core/pine_udt.py +69 -0
  72. pynecore/core/plugin/__init__.py +394 -0
  73. pynecore/core/plugin/broker.py +781 -0
  74. pynecore/core/plugin/cli.py +96 -0
  75. pynecore/core/plugin/live_provider.py +208 -0
  76. pynecore/core/plugin/provider.py +331 -0
  77. pynecore/core/provider_string.py +148 -0
  78. pynecore/core/random.py +40 -0
  79. pynecore/core/resampler.py +686 -0
  80. pynecore/core/safe_convert.py +64 -0
  81. pynecore/core/script.py +1011 -0
  82. pynecore/core/script_runner.py +3202 -0
  83. pynecore/core/security.py +1749 -0
  84. pynecore/core/security_process.py +1253 -0
  85. pynecore/core/security_shm.py +456 -0
  86. pynecore/core/series.py +417 -0
  87. pynecore/core/strategy_stats.py +669 -0
  88. pynecore/core/symbol_map.py +134 -0
  89. pynecore/core/syminfo.py +505 -0
  90. pynecore/core/viz.py +591 -0
  91. pynecore/lib/__init__.py +1771 -0
  92. pynecore/lib/_fixnan.py +32 -0
  93. pynecore/lib/_math_stateful.py +202 -0
  94. pynecore/lib/_timeframe_change.py +101 -0
  95. pynecore/lib/adjustment.py +6 -0
  96. pynecore/lib/alert.py +39 -0
  97. pynecore/lib/alert.pyi +14 -0
  98. pynecore/lib/array.py +1051 -0
  99. pynecore/lib/barmerge.py +60 -0
  100. pynecore/lib/barstate.py +30 -0
  101. pynecore/lib/box.py +415 -0
  102. pynecore/lib/chart.py +128 -0
  103. pynecore/lib/color.py +152 -0
  104. pynecore/lib/color.pyi +50 -0
  105. pynecore/lib/currency.py +62 -0
  106. pynecore/lib/dayofweek.py +36 -0
  107. pynecore/lib/dayofweek.pyi +18 -0
  108. pynecore/lib/display.py +8 -0
  109. pynecore/lib/dividends.py +9 -0
  110. pynecore/lib/earnings.py +11 -0
  111. pynecore/lib/extend.py +6 -0
  112. pynecore/lib/font.py +5 -0
  113. pynecore/lib/footprint.py +79 -0
  114. pynecore/lib/format.py +11 -0
  115. pynecore/lib/hline.py +67 -0
  116. pynecore/lib/hline.pyi +24 -0
  117. pynecore/lib/label.py +409 -0
  118. pynecore/lib/line.py +433 -0
  119. pynecore/lib/linefill.py +93 -0
  120. pynecore/lib/location.py +11 -0
  121. pynecore/lib/log.py +362 -0
  122. pynecore/lib/map.py +150 -0
  123. pynecore/lib/math.py +385 -0
  124. pynecore/lib/matrix.py +708 -0
  125. pynecore/lib/order.py +8 -0
  126. pynecore/lib/pivotpointtype.py +8 -0
  127. pynecore/lib/plot.py +95 -0
  128. pynecore/lib/plot.pyi +33 -0
  129. pynecore/lib/polyline.py +91 -0
  130. pynecore/lib/position.py +15 -0
  131. pynecore/lib/request.py +281 -0
  132. pynecore/lib/runtime.py +5 -0
  133. pynecore/lib/scale.py +9 -0
  134. pynecore/lib/session.py +267 -0
  135. pynecore/lib/session.pyi +12 -0
  136. pynecore/lib/shape.py +18 -0
  137. pynecore/lib/size.py +12 -0
  138. pynecore/lib/splits.py +4 -0
  139. pynecore/lib/strategy/__init__.py +4778 -0
  140. pynecore/lib/strategy/closedtrades.py +347 -0
  141. pynecore/lib/strategy/closedtrades.pyi +53 -0
  142. pynecore/lib/strategy/commission.py +9 -0
  143. pynecore/lib/strategy/direction.py +9 -0
  144. pynecore/lib/strategy/oca.py +13 -0
  145. pynecore/lib/strategy/opentrades.py +281 -0
  146. pynecore/lib/strategy/opentrades.pyi +49 -0
  147. pynecore/lib/strategy/risk.py +109 -0
  148. pynecore/lib/string.py +649 -0
  149. pynecore/lib/syminfo.py +84 -0
  150. pynecore/lib/ta.py +2230 -0
  151. pynecore/lib/table.py +290 -0
  152. pynecore/lib/text.py +17 -0
  153. pynecore/lib/ticker.py +207 -0
  154. pynecore/lib/timeframe.py +293 -0
  155. pynecore/lib/volume_row.py +67 -0
  156. pynecore/lib/xloc.py +4 -0
  157. pynecore/lib/yloc.py +5 -0
  158. pynecore/providers/__init__.py +0 -0
  159. pynecore/providers/ccxt.py +664 -0
  160. pynecore/providers/replay.py +187 -0
  161. pynecore/pynesys/__init__.py +0 -0
  162. pynecore/pynesys/api.py +498 -0
  163. pynecore/pynesys/compiler.py +112 -0
  164. pynecore/standalone.py +99 -0
  165. pynecore/testing/__init__.py +1 -0
  166. pynecore/testing/broker_lab/__init__.py +41 -0
  167. pynecore/testing/broker_lab/__main__.py +5 -0
  168. pynecore/testing/broker_lab/cli.py +87 -0
  169. pynecore/testing/broker_lab/generate.py +47 -0
  170. pynecore/testing/broker_lab/model.py +84 -0
  171. pynecore/testing/broker_lab/reference.py +645 -0
  172. pynecore/testing/broker_lab/runner.py +372 -0
  173. pynecore/testing/broker_lab/scheduler.py +50 -0
  174. pynecore/testing/broker_lab/subprocess.py +73 -0
  175. pynecore/transformers/__init__.py +0 -0
  176. pynecore/transformers/builtin_shadow.py +136 -0
  177. pynecore/transformers/closure_arguments_transformer.py +428 -0
  178. pynecore/transformers/display_rewrite.py +140 -0
  179. pynecore/transformers/dynamic_default.py +147 -0
  180. pynecore/transformers/function_isolation.py +757 -0
  181. pynecore/transformers/import_lifter.py +61 -0
  182. pynecore/transformers/import_normalizer.py +328 -0
  183. pynecore/transformers/inline_series_hoist.py +178 -0
  184. pynecore/transformers/input_transformer.py +175 -0
  185. pynecore/transformers/lib_series.py +201 -0
  186. pynecore/transformers/locations.py +70 -0
  187. pynecore/transformers/module_properties.json +3387 -0
  188. pynecore/transformers/module_property.py +221 -0
  189. pynecore/transformers/ne_guard.py +70 -0
  190. pynecore/transformers/persistent.py +320 -0
  191. pynecore/transformers/persistent_series.py +76 -0
  192. pynecore/transformers/safe_convert_transformer.py +97 -0
  193. pynecore/transformers/safe_division_transformer.py +95 -0
  194. pynecore/transformers/script_requirements.py +308 -0
  195. pynecore/transformers/security.py +752 -0
  196. pynecore/transformers/security_instantiation.py +274 -0
  197. pynecore/transformers/series.py +275 -0
  198. pynecore/transformers/slot_layout.py +381 -0
  199. pynecore/transformers/type_checking_stripper.py +25 -0
  200. pynecore/transformers/unused_series_detector.py +267 -0
  201. pynecore/types/__init__.py +21 -0
  202. pynecore/types/alert.py +5 -0
  203. pynecore/types/barmerge.py +5 -0
  204. pynecore/types/base.py +39 -0
  205. pynecore/types/box.py +37 -0
  206. pynecore/types/chart.py +17 -0
  207. pynecore/types/color.py +107 -0
  208. pynecore/types/currency.py +5 -0
  209. pynecore/types/datetime.py +6 -0
  210. pynecore/types/display.py +5 -0
  211. pynecore/types/dividends.py +5 -0
  212. pynecore/types/earnings.py +5 -0
  213. pynecore/types/extend.py +5 -0
  214. pynecore/types/font.py +5 -0
  215. pynecore/types/footprint.py +41 -0
  216. pynecore/types/format.py +5 -0
  217. pynecore/types/hline.py +24 -0
  218. pynecore/types/ib_persistent.py +8 -0
  219. pynecore/types/ib_persistent.pyi +10 -0
  220. pynecore/types/label.py +35 -0
  221. pynecore/types/line.py +32 -0
  222. pynecore/types/linefill.py +13 -0
  223. pynecore/types/location.py +5 -0
  224. pynecore/types/matrix.py +999 -0
  225. pynecore/types/na.py +237 -0
  226. pynecore/types/na.pyi +83 -0
  227. pynecore/types/ohlcv.py +12 -0
  228. pynecore/types/order.py +5 -0
  229. pynecore/types/persistent.py +8 -0
  230. pynecore/types/persistent.pyi +13 -0
  231. pynecore/types/pine_types.py +11 -0
  232. pynecore/types/pine_types.pyi +15 -0
  233. pynecore/types/pivotpointtype.py +5 -0
  234. pynecore/types/plot.py +12 -0
  235. pynecore/types/plot_meta.py +60 -0
  236. pynecore/types/polyline.py +40 -0
  237. pynecore/types/position.py +5 -0
  238. pynecore/types/scale.py +5 -0
  239. pynecore/types/script_type.py +15 -0
  240. pynecore/types/series.py +23 -0
  241. pynecore/types/series.pyi +19 -0
  242. pynecore/types/session.py +35 -0
  243. pynecore/types/shape.py +5 -0
  244. pynecore/types/size.py +5 -0
  245. pynecore/types/source.py +33 -0
  246. pynecore/types/splits.py +5 -0
  247. pynecore/types/strategy.py +45 -0
  248. pynecore/types/table.py +87 -0
  249. pynecore/types/text.py +13 -0
  250. pynecore/types/type_checker.py +7 -0
  251. pynecore/types/type_checker.pyi +48 -0
  252. pynecore/types/volume_row.py +36 -0
  253. pynecore/types/weekdays.py +11 -0
  254. pynecore/types/xloc.py +5 -0
  255. pynecore/types/yloc.py +5 -0
  256. pynecore/utils/__init__.py +0 -0
  257. pynecore/utils/file_utils.py +50 -0
  258. pynecore/utils/rich/__init__.py +0 -0
  259. pynecore/utils/rich/date_column.py +25 -0
  260. pynecore/utils/sequence_view.py +92 -0
  261. pynecore/utils/stdlib_checker.py +17 -0
@@ -0,0 +1,112 @@
1
+ """
2
+ Core compilation service for programmatic use.
3
+ """
4
+ from pathlib import Path
5
+
6
+ from pynecore.pynesys.api import (APIClient, APIError, AuthError, RateLimitError, CompilationError, UsageResponse,
7
+ TokenValidationResponse)
8
+ from pynecore.utils.file_utils import is_updated
9
+
10
+
11
+ class PyneComp:
12
+ """
13
+ Compiler through the PyneSys API.
14
+ """
15
+
16
+ api_client: APIClient
17
+
18
+ def __init__(self, api_key, base_url="https://api.pynesys.io", timeout=30):
19
+ """
20
+ Initialize the compilation service.
21
+
22
+ :param api_key: PyneSys API key
23
+ :param base_url: Base URL for the API
24
+ :param timeout: Request timeout in seconds
25
+ """
26
+ self.api_client = APIClient(api_key=api_key, base_url=base_url, timeout=timeout)
27
+
28
+ def compile(self, pine_path: Path, output_path: Path | None = None,
29
+ force: bool = False, strict: bool = False) -> Path:
30
+ """
31
+ Compile a .pine file to Python.
32
+
33
+ :param pine_path: Path to the .pine file
34
+ :param output_path: Optional output path (defaults to .py extension)
35
+ :param force: Force recompilation even if file hasn't changed
36
+ :param strict: Enable strict compilation mode
37
+ :return: Path to the compiled .py file
38
+ :raises FileNotFoundError: If pine file doesn't exist
39
+ :raises CompilationError: If compilation fails
40
+ :raises APIError: If API request fails
41
+ """
42
+ # Validate input file
43
+ if not pine_path.exists():
44
+ raise FileNotFoundError(f"Pine file not found: {pine_path}")
45
+
46
+ if pine_path.suffix != '.pine':
47
+ raise ValueError(f"This file format isn't supported: {pine_path.suffix}. "
48
+ f"Only .pine files can be compiled!")
49
+
50
+ # Determine output path
51
+ resolved_output: Path = output_path if output_path is not None else pine_path.with_suffix('.py')
52
+
53
+ # Check if compilation is needed (unless forced)
54
+ if not force and not self.needs_compilation(pine_path, resolved_output):
55
+ return resolved_output
56
+
57
+ # Read Pine Script content
58
+ try:
59
+ with open(pine_path, 'r', encoding='utf-8') as f:
60
+ script_content = f.read()
61
+ except IOError as e:
62
+ raise IOError(f"Error reading Pine file {pine_path}: {e}")
63
+
64
+ # Compile via API
65
+ try:
66
+ response = self.api_client.compile_script(script_content, strict=strict)
67
+
68
+ if not response.success:
69
+ raise CompilationError(
70
+ f"Compilation failed: {response.error_message}",
71
+ status_code=response.status_code,
72
+ validation_errors=response.validation_errors
73
+ )
74
+
75
+ # Write compiled code to output file
76
+ assert response.compiled_code is not None
77
+ resolved_output.parent.mkdir(parents=True, exist_ok=True)
78
+ with open(resolved_output, 'w', encoding='utf-8') as f:
79
+ f.write(response.compiled_code)
80
+
81
+ # No need to update tracking info with mtime approach
82
+ return resolved_output
83
+
84
+ except (APIError, AuthError, RateLimitError, CompilationError):
85
+ # Re-raise API-related errors as-is
86
+ raise
87
+ except Exception as e:
88
+ # Wrap unexpected errors
89
+ raise APIError(f"Unexpected error during compilation: {e}")
90
+
91
+ def get_usage(self) -> UsageResponse:
92
+ """
93
+ Get current usage statistics and limits for the authenticated user.
94
+ """
95
+ return self.api_client.get_usage()
96
+
97
+ def validate_api_key(self) -> TokenValidationResponse:
98
+ """
99
+ Validate API key.
100
+ """
101
+ return self.api_client.verify_token_local()
102
+
103
+ @staticmethod
104
+ def needs_compilation(pine_file_path: Path, output_file_path: Path) -> bool:
105
+ """
106
+ Check if a .pine file needs compilation using modification time comparison.
107
+
108
+ :param pine_file_path: Path to the .pine file
109
+ :param output_file_path: Path to the compiled .py file
110
+ :return: True if compilation is needed, False otherwise
111
+ """
112
+ return is_updated(pine_file_path, output_file_path)
pynecore/standalone.py ADDED
@@ -0,0 +1,99 @@
1
+ """Standalone runner for PyneComp-compiled Pyne code."""
2
+ import sys
3
+ from pathlib import Path
4
+
5
+
6
+ def run(script_file: str) -> None:
7
+ """
8
+ Run compiled Pyne code in standalone mode.
9
+
10
+ Enables ``python script.py data.csv`` without a workdir or the ``pyne`` CLI.
11
+
12
+ :param script_file: The ``__file__`` of the calling script
13
+ """
14
+ if len(sys.argv) < 2:
15
+ script_name = Path(script_file).name
16
+ print(f"Usage: python {script_name} <data_file>", file=sys.stderr)
17
+ print(f"\n data_file: Path to CSV or OHLCV data file", file=sys.stderr)
18
+ sys.exit(1)
19
+
20
+ data_arg = sys.argv[1]
21
+ data_path = Path(data_arg).resolve()
22
+ script_path = Path(script_file).resolve()
23
+
24
+ if not data_path.exists():
25
+ print(f"Error: Data file '{data_arg}' not found", file=sys.stderr)
26
+ sys.exit(1)
27
+
28
+ import shutil
29
+ import tempfile
30
+ from pynecore.core.data_converter import DataConverter, DataFormatError, ConversionError
31
+ from pynecore.core.ohlcv_file import OHLCVReader
32
+ from pynecore.core.syminfo import SymInfo
33
+ from pynecore.core.script_runner import ScriptRunner
34
+
35
+ temp_dir = None
36
+ ohlcv_path = data_path
37
+
38
+ try:
39
+ # CSV/TXT/JSON → OHLCV conversion in temp directory
40
+ if data_path.suffix != '.ohlcv':
41
+ try:
42
+ temp_dir = tempfile.mkdtemp(prefix="pyne_")
43
+ # Copy preserves filename for guess_symbol_from_filename heuristics
44
+ temp_copy = Path(temp_dir) / data_path.name
45
+ shutil.copy2(data_path, temp_copy)
46
+
47
+ converter = DataConverter()
48
+ detected_symbol, detected_provider = DataConverter.guess_symbol_from_filename(
49
+ data_path
50
+ )
51
+ if not detected_symbol:
52
+ detected_symbol = data_path.stem.upper()
53
+
54
+ print(f"Converting {data_path.name}...", file=sys.stderr)
55
+ converter.convert_to_ohlcv(
56
+ temp_copy, provider=detected_provider,
57
+ symbol=detected_symbol, force=True
58
+ )
59
+ ohlcv_path = temp_copy.with_suffix('.ohlcv')
60
+ except (DataFormatError, ConversionError) as e:
61
+ print(f"Error: {e}", file=sys.stderr)
62
+ sys.exit(1)
63
+
64
+ # Load symbol info
65
+ toml_path = ohlcv_path.with_suffix('.toml')
66
+ try:
67
+ syminfo = SymInfo.load_toml(toml_path)
68
+ except FileNotFoundError:
69
+ print(f"Error: Symbol info '{toml_path.name}' not found", file=sys.stderr)
70
+ sys.exit(1)
71
+
72
+ # Output paths next to script
73
+ out_dir = script_path.parent
74
+ plot_path = out_dir / f"{script_path.stem}.csv"
75
+ trade_path = out_dir / f"{script_path.stem}_trades.csv"
76
+ strat_path = out_dir / f"{script_path.stem}_strat.csv"
77
+
78
+ # Run using the standard ScriptRunner
79
+ with OHLCVReader(ohlcv_path) as reader:
80
+ start_ts: int = reader.start_timestamp # type: ignore[assignment]
81
+ end_ts: int = reader.end_timestamp # type: ignore[assignment]
82
+ size = reader.get_size(start_ts, end_ts)
83
+ ohlcv_iter = reader.read_from(start_ts, end_ts)
84
+ print(
85
+ f"Running {script_path.name} on {data_path.stem} ({size} bars)...",
86
+ file=sys.stderr
87
+ )
88
+
89
+ runner = ScriptRunner(
90
+ script_path, ohlcv_iter, syminfo, last_bar_index=size - 1,
91
+ plot_path=plot_path, strat_path=strat_path, trade_path=trade_path
92
+ )
93
+ runner.run()
94
+
95
+ print(f"Done. Output: {plot_path}", file=sys.stderr)
96
+
97
+ finally:
98
+ if temp_dir:
99
+ shutil.rmtree(temp_dir, ignore_errors=True)
@@ -0,0 +1 @@
1
+ """Testing helpers for PyneCore plugin authors."""
@@ -0,0 +1,41 @@
1
+ """Public API for deterministic offline broker conformance testing."""
2
+
3
+ from .generate import pairwise_cases
4
+ from .model import (
5
+ Scenario,
6
+ ScenarioInvariantError,
7
+ ScenarioResult,
8
+ Step,
9
+ VenueProfile,
10
+ )
11
+ from .reference import (
12
+ HedgedReferenceVenueProfile,
13
+ ReferenceBroker,
14
+ ReferenceVenueProfile,
15
+ VenueOrder,
16
+ VenueState,
17
+ )
18
+ from .runner import RunRuntime, ScenarioRunner
19
+ from .scheduler import DeterministicScheduler, ScheduledEvent
20
+ from .subprocess import SubprocessResult, run_subprocess, temporary_entry_point
21
+
22
+ __all__ = [
23
+ "DeterministicScheduler",
24
+ "HedgedReferenceVenueProfile",
25
+ "ReferenceBroker",
26
+ "ReferenceVenueProfile",
27
+ "RunRuntime",
28
+ "Scenario",
29
+ "ScenarioInvariantError",
30
+ "ScenarioResult",
31
+ "ScenarioRunner",
32
+ "ScheduledEvent",
33
+ "Step",
34
+ "SubprocessResult",
35
+ "VenueProfile",
36
+ "VenueOrder",
37
+ "VenueState",
38
+ "pairwise_cases",
39
+ "run_subprocess",
40
+ "temporary_entry_point",
41
+ ]
@@ -0,0 +1,5 @@
1
+ """Module entry point for the opt-in broker lab."""
2
+
3
+ from .cli import main
4
+
5
+ raise SystemExit(main())
@@ -0,0 +1,87 @@
1
+ """Opt-in command-line runner for broker conformance suites."""
2
+
3
+ import argparse
4
+ from collections.abc import Sequence
5
+ import importlib.util
6
+ import json
7
+ import sys
8
+ from pathlib import Path
9
+ from types import ModuleType
10
+
11
+ from .model import Scenario
12
+ from .runner import ScenarioRunner
13
+
14
+
15
+ def _load_suite(path: Path) -> ModuleType:
16
+ spec = importlib.util.spec_from_file_location(f"pyne_broker_lab_suite_{path.stem}", path)
17
+ if spec is None or spec.loader is None:
18
+ raise RuntimeError(f"cannot load broker-lab suite: {path}")
19
+ module = importlib.util.module_from_spec(spec)
20
+ sys.modules[spec.name] = module
21
+ spec.loader.exec_module(module)
22
+ return module
23
+
24
+
25
+ def _scenarios(module: ModuleType, mode: str, seed: int) -> Sequence[Scenario]:
26
+ builder = getattr(module, "build_suite", None)
27
+ if builder is not None:
28
+ return tuple(builder(mode=mode, seed=seed))
29
+ scenarios = tuple(getattr(module, "SCENARIOS", ()))
30
+ if mode == "smoke":
31
+ return tuple(s for s in scenarios if "smoke" in s.tags)
32
+ return scenarios
33
+
34
+
35
+ def main(argv: Sequence[str] | None = None) -> int:
36
+ parser = argparse.ArgumentParser(prog="python -m pynecore.testing.broker_lab")
37
+ sub = parser.add_subparsers(dest="command", required=True)
38
+ run = sub.add_parser("run", help="run an opt-in broker-lab suite")
39
+ run.add_argument("suite", type=Path)
40
+ run.add_argument("--mode", choices=("smoke", "extended"), default="smoke")
41
+ run.add_argument("--seed", type=int, default=1337)
42
+ run.add_argument("--scenario")
43
+ run.add_argument("--report", type=Path)
44
+ args = parser.parse_args(argv)
45
+
46
+ module = _load_suite(args.suite.resolve())
47
+ scenarios = list(_scenarios(module, args.mode, args.seed))
48
+ if args.scenario:
49
+ scenarios = [scenario for scenario in scenarios if scenario.name == args.scenario]
50
+ if not scenarios:
51
+ parser.error(f"unknown scenario: {args.scenario}")
52
+ results = [ScenarioRunner().run(scenario) for scenario in scenarios]
53
+ for result in results:
54
+ status = "PASS" if result.passed else "FAIL"
55
+ print(f"{status} {result.name} seed={result.seed}")
56
+ if result.violation:
57
+ print(f" {result.violation}")
58
+ print(
59
+ " reproduce: python -m pynecore.testing.broker_lab run "
60
+ f"{args.suite} --mode {args.mode} {result.reproduction}"
61
+ )
62
+ for step in result.minimized_steps:
63
+ print(f" {step.kind} run={step.run} values={step.values!r}")
64
+ if args.report:
65
+ args.report.parent.mkdir(parents=True, exist_ok=True)
66
+ args.report.write_text(
67
+ json.dumps(
68
+ [
69
+ {
70
+ "name": result.name,
71
+ "passed": result.passed,
72
+ "seed": result.seed,
73
+ "violation": result.violation,
74
+ "reproduction": result.reproduction,
75
+ "minimized_steps": [
76
+ {"kind": s.kind, "run": s.run, "values": s.values} for s in result.minimized_steps
77
+ ],
78
+ }
79
+ for result in results
80
+ ],
81
+ indent=2,
82
+ sort_keys=True,
83
+ )
84
+ + "\n",
85
+ encoding="utf-8",
86
+ )
87
+ return 0 if all(result.passed for result in results) else 1
@@ -0,0 +1,47 @@
1
+ """Deterministic scenario generation without third-party dependencies."""
2
+
3
+ import random
4
+ from collections.abc import Iterable, Mapping
5
+ from itertools import combinations, product
6
+ from typing import Any
7
+
8
+
9
+ def pairwise_cases(
10
+ axes: Mapping[str, Iterable[Any]],
11
+ *,
12
+ seed: int = 0,
13
+ ) -> list[dict[str, Any]]:
14
+ """Return a deterministic greedy pairwise covering array.
15
+
16
+ Every value pair from every pair of axes appears in at least one returned
17
+ case. The seed only breaks equal-coverage ties, so the same seed is exactly
18
+ reproducible across processes.
19
+ """
20
+ names = list(axes)
21
+ values = {name: tuple(axes[name]) for name in names}
22
+ if any(not value for value in values.values()):
23
+ raise ValueError("pairwise axes must not be empty")
24
+ if not names:
25
+ return [{}]
26
+ all_cases = [dict(zip(names, case)) for case in product(*(values[n] for n in names))]
27
+ if len(names) == 1:
28
+ return all_cases
29
+
30
+ uncovered = {(a, repr(av), b, repr(bv)) for a, b in combinations(names, 2) for av in values[a] for bv in values[b]}
31
+ rng = random.Random(seed)
32
+ tie_order = list(range(len(all_cases)))
33
+ rng.shuffle(tie_order)
34
+ selected: list[dict[str, Any]] = []
35
+ while uncovered:
36
+ best_idx = max(
37
+ tie_order,
38
+ key=lambda idx: sum(
39
+ (a, repr(all_cases[idx][a]), b, repr(all_cases[idx][b])) in uncovered for a, b in combinations(names, 2)
40
+ ),
41
+ )
42
+ case = all_cases[best_idx]
43
+ selected.append(case)
44
+ for a, b in combinations(names, 2):
45
+ uncovered.discard((a, repr(case[a]), b, repr(case[b])))
46
+ tie_order.remove(best_idx)
47
+ return selected
@@ -0,0 +1,84 @@
1
+ """Public scenario model for the offline broker conformance lab."""
2
+
3
+ from collections.abc import Callable, Sequence
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+ from typing import Any, Protocol
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class Step:
11
+ """One deterministic broker scenario transition."""
12
+
13
+ kind: str
14
+ run: str = "main"
15
+ values: dict[str, Any] = field(default_factory=dict)
16
+ check_invariants: bool = True
17
+
18
+
19
+ class VenueProfile(Protocol):
20
+ """Venue semantics consumed by :class:`ScenarioRunner`.
21
+
22
+ Profiles may return a real plugin instance or a deliberately small broker
23
+ implementation. Plugin suites normally return the real plugin with only its
24
+ HTTP, WebSocket, or wire transport replaced.
25
+ """
26
+
27
+ plugin_name: str
28
+ account_id: str
29
+ symbol: str
30
+ timeframe: str
31
+
32
+ def create_broker(self, run_name: str, store_ctx: Any) -> Any:
33
+ """Create the broker used by one logical run."""
34
+ ...
35
+
36
+ def handle_step(self, runner: Any, step: Step) -> bool:
37
+ """Apply a profile-specific step and return whether it was handled."""
38
+ ...
39
+
40
+ def check_invariants(self, runner: Any) -> Sequence[str]:
41
+ """Return invariant violations after the current transition."""
42
+ ...
43
+
44
+ def close(self) -> None:
45
+ """Release profile-owned resources."""
46
+ ...
47
+
48
+
49
+ ProfileFactory = Callable[[], VenueProfile]
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class Scenario:
54
+ """A reproducible sequence executed against a fresh venue profile."""
55
+
56
+ name: str
57
+ profile_factory: ProfileFactory
58
+ steps: tuple[Step, ...]
59
+ runs: tuple[str, ...] = ("main",)
60
+ seed: int = 0
61
+ tags: frozenset[str] = frozenset({"smoke"})
62
+ expected_violation: str | None = None
63
+
64
+
65
+ @dataclass(frozen=True)
66
+ class ScenarioResult:
67
+ """Outcome and reproduction data for one scenario."""
68
+
69
+ name: str
70
+ passed: bool
71
+ seed: int
72
+ executed_steps: tuple[Step, ...]
73
+ violation: str | None = None
74
+ minimized_steps: tuple[Step, ...] = ()
75
+ artifact_dir: Path | None = None
76
+
77
+ @property
78
+ def reproduction(self) -> str:
79
+ """Return the stable seed fragment used by CLI reproduction."""
80
+ return f"--scenario {self.name} --seed {self.seed}"
81
+
82
+
83
+ class ScenarioInvariantError(AssertionError):
84
+ """Raised when a step violates a broker-lab invariant."""