opencode-pyneruntime 6.6.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- opencode_pyneruntime-6.6.4.dist-info/METADATA +281 -0
- opencode_pyneruntime-6.6.4.dist-info/RECORD +261 -0
- opencode_pyneruntime-6.6.4.dist-info/WHEEL +5 -0
- opencode_pyneruntime-6.6.4.dist-info/entry_points.txt +6 -0
- opencode_pyneruntime-6.6.4.dist-info/licenses/LICENSE +201 -0
- opencode_pyneruntime-6.6.4.dist-info/licenses/NOTICE +21 -0
- opencode_pyneruntime-6.6.4.dist-info/top_level.txt +1 -0
- pynecore/__init__.py +6 -0
- pynecore/cli/__init__.py +2 -0
- pynecore/cli/app.py +238 -0
- pynecore/cli/commands/__init__.py +343 -0
- pynecore/cli/commands/benchmark.py +186 -0
- pynecore/cli/commands/compile.py +198 -0
- pynecore/cli/commands/data.py +857 -0
- pynecore/cli/commands/debug.py +63 -0
- pynecore/cli/commands/optimize.py +956 -0
- pynecore/cli/commands/plugin.py +242 -0
- pynecore/cli/commands/run.py +2006 -0
- pynecore/cli/pluggable.py +132 -0
- pynecore/cli/utils/__init__.py +0 -0
- pynecore/cli/utils/api_error_handler.py +168 -0
- pynecore/cli/utils/broker_picker.py +330 -0
- pynecore/cli/utils/error_hook.py +28 -0
- pynecore/cli/utils/keyreader.py +178 -0
- pynecore/cli/utils/provider_picker.py +19 -0
- pynecore/cli/utils/symbol_browser.py +1149 -0
- pynecore/core/__init__.py +0 -0
- pynecore/core/aggregator.py +257 -0
- pynecore/core/bar_magnifier.py +168 -0
- pynecore/core/broker/__init__.py +64 -0
- pynecore/core/broker/defaults.py +113 -0
- pynecore/core/broker/disappearance.py +927 -0
- pynecore/core/broker/emulator.py +345 -0
- pynecore/core/broker/exceptions.py +346 -0
- pynecore/core/broker/idempotency.py +401 -0
- pynecore/core/broker/intent_builder.py +334 -0
- pynecore/core/broker/journal.py +1785 -0
- pynecore/core/broker/models.py +1600 -0
- pynecore/core/broker/native_failsafe_manager.py +1436 -0
- pynecore/core/broker/one_way_emulator.py +1128 -0
- pynecore/core/broker/position.py +787 -0
- pynecore/core/broker/run_identity.py +126 -0
- pynecore/core/broker/software_entry_stop_engine.py +351 -0
- pynecore/core/broker/software_partial_bracket_engine.py +1379 -0
- pynecore/core/broker/spot_inventory.py +1327 -0
- pynecore/core/broker/storage.py +2655 -0
- pynecore/core/broker/store_helpers.py +2161 -0
- pynecore/core/broker/sync_engine.py +16070 -0
- pynecore/core/broker/validation.py +382 -0
- pynecore/core/class_property.py +7 -0
- pynecore/core/config.py +392 -0
- pynecore/core/csv_file.py +547 -0
- pynecore/core/currency.py +262 -0
- pynecore/core/data_converter.py +1002 -0
- pynecore/core/datetime.py +296 -0
- pynecore/core/download_info.py +71 -0
- pynecore/core/download_runner.py +274 -0
- pynecore/core/htf_aggregator.py +181 -0
- pynecore/core/import_hook.py +358 -0
- pynecore/core/instance_state.py +494 -0
- pynecore/core/live_ltf_collector.py +442 -0
- pynecore/core/live_ltf_window.py +189 -0
- pynecore/core/live_runner.py +1347 -0
- pynecore/core/module_property.py +26 -0
- pynecore/core/ohlcv_file.py +1888 -0
- pynecore/core/overload.py +371 -0
- pynecore/core/pine_cast.py +113 -0
- pynecore/core/pine_export.py +95 -0
- pynecore/core/pine_method.py +244 -0
- pynecore/core/pine_range.py +86 -0
- pynecore/core/pine_udt.py +69 -0
- pynecore/core/plugin/__init__.py +394 -0
- pynecore/core/plugin/broker.py +781 -0
- pynecore/core/plugin/cli.py +96 -0
- pynecore/core/plugin/live_provider.py +208 -0
- pynecore/core/plugin/provider.py +331 -0
- pynecore/core/provider_string.py +148 -0
- pynecore/core/random.py +40 -0
- pynecore/core/resampler.py +686 -0
- pynecore/core/safe_convert.py +64 -0
- pynecore/core/script.py +1011 -0
- pynecore/core/script_runner.py +3202 -0
- pynecore/core/security.py +1749 -0
- pynecore/core/security_process.py +1253 -0
- pynecore/core/security_shm.py +456 -0
- pynecore/core/series.py +417 -0
- pynecore/core/strategy_stats.py +669 -0
- pynecore/core/symbol_map.py +134 -0
- pynecore/core/syminfo.py +505 -0
- pynecore/core/viz.py +591 -0
- pynecore/lib/__init__.py +1771 -0
- pynecore/lib/_fixnan.py +32 -0
- pynecore/lib/_math_stateful.py +202 -0
- pynecore/lib/_timeframe_change.py +101 -0
- pynecore/lib/adjustment.py +6 -0
- pynecore/lib/alert.py +39 -0
- pynecore/lib/alert.pyi +14 -0
- pynecore/lib/array.py +1051 -0
- pynecore/lib/barmerge.py +60 -0
- pynecore/lib/barstate.py +30 -0
- pynecore/lib/box.py +415 -0
- pynecore/lib/chart.py +128 -0
- pynecore/lib/color.py +152 -0
- pynecore/lib/color.pyi +50 -0
- pynecore/lib/currency.py +62 -0
- pynecore/lib/dayofweek.py +36 -0
- pynecore/lib/dayofweek.pyi +18 -0
- pynecore/lib/display.py +8 -0
- pynecore/lib/dividends.py +9 -0
- pynecore/lib/earnings.py +11 -0
- pynecore/lib/extend.py +6 -0
- pynecore/lib/font.py +5 -0
- pynecore/lib/footprint.py +79 -0
- pynecore/lib/format.py +11 -0
- pynecore/lib/hline.py +67 -0
- pynecore/lib/hline.pyi +24 -0
- pynecore/lib/label.py +409 -0
- pynecore/lib/line.py +433 -0
- pynecore/lib/linefill.py +93 -0
- pynecore/lib/location.py +11 -0
- pynecore/lib/log.py +362 -0
- pynecore/lib/map.py +150 -0
- pynecore/lib/math.py +385 -0
- pynecore/lib/matrix.py +708 -0
- pynecore/lib/order.py +8 -0
- pynecore/lib/pivotpointtype.py +8 -0
- pynecore/lib/plot.py +95 -0
- pynecore/lib/plot.pyi +33 -0
- pynecore/lib/polyline.py +91 -0
- pynecore/lib/position.py +15 -0
- pynecore/lib/request.py +281 -0
- pynecore/lib/runtime.py +5 -0
- pynecore/lib/scale.py +9 -0
- pynecore/lib/session.py +267 -0
- pynecore/lib/session.pyi +12 -0
- pynecore/lib/shape.py +18 -0
- pynecore/lib/size.py +12 -0
- pynecore/lib/splits.py +4 -0
- pynecore/lib/strategy/__init__.py +4778 -0
- pynecore/lib/strategy/closedtrades.py +347 -0
- pynecore/lib/strategy/closedtrades.pyi +53 -0
- pynecore/lib/strategy/commission.py +9 -0
- pynecore/lib/strategy/direction.py +9 -0
- pynecore/lib/strategy/oca.py +13 -0
- pynecore/lib/strategy/opentrades.py +281 -0
- pynecore/lib/strategy/opentrades.pyi +49 -0
- pynecore/lib/strategy/risk.py +109 -0
- pynecore/lib/string.py +649 -0
- pynecore/lib/syminfo.py +84 -0
- pynecore/lib/ta.py +2230 -0
- pynecore/lib/table.py +290 -0
- pynecore/lib/text.py +17 -0
- pynecore/lib/ticker.py +207 -0
- pynecore/lib/timeframe.py +293 -0
- pynecore/lib/volume_row.py +67 -0
- pynecore/lib/xloc.py +4 -0
- pynecore/lib/yloc.py +5 -0
- pynecore/providers/__init__.py +0 -0
- pynecore/providers/ccxt.py +664 -0
- pynecore/providers/replay.py +187 -0
- pynecore/pynesys/__init__.py +0 -0
- pynecore/pynesys/api.py +498 -0
- pynecore/pynesys/compiler.py +112 -0
- pynecore/standalone.py +99 -0
- pynecore/testing/__init__.py +1 -0
- pynecore/testing/broker_lab/__init__.py +41 -0
- pynecore/testing/broker_lab/__main__.py +5 -0
- pynecore/testing/broker_lab/cli.py +87 -0
- pynecore/testing/broker_lab/generate.py +47 -0
- pynecore/testing/broker_lab/model.py +84 -0
- pynecore/testing/broker_lab/reference.py +645 -0
- pynecore/testing/broker_lab/runner.py +372 -0
- pynecore/testing/broker_lab/scheduler.py +50 -0
- pynecore/testing/broker_lab/subprocess.py +73 -0
- pynecore/transformers/__init__.py +0 -0
- pynecore/transformers/builtin_shadow.py +136 -0
- pynecore/transformers/closure_arguments_transformer.py +428 -0
- pynecore/transformers/display_rewrite.py +140 -0
- pynecore/transformers/dynamic_default.py +147 -0
- pynecore/transformers/function_isolation.py +757 -0
- pynecore/transformers/import_lifter.py +61 -0
- pynecore/transformers/import_normalizer.py +328 -0
- pynecore/transformers/inline_series_hoist.py +178 -0
- pynecore/transformers/input_transformer.py +175 -0
- pynecore/transformers/lib_series.py +201 -0
- pynecore/transformers/locations.py +70 -0
- pynecore/transformers/module_properties.json +3387 -0
- pynecore/transformers/module_property.py +221 -0
- pynecore/transformers/ne_guard.py +70 -0
- pynecore/transformers/persistent.py +320 -0
- pynecore/transformers/persistent_series.py +76 -0
- pynecore/transformers/safe_convert_transformer.py +97 -0
- pynecore/transformers/safe_division_transformer.py +95 -0
- pynecore/transformers/script_requirements.py +308 -0
- pynecore/transformers/security.py +752 -0
- pynecore/transformers/security_instantiation.py +274 -0
- pynecore/transformers/series.py +275 -0
- pynecore/transformers/slot_layout.py +381 -0
- pynecore/transformers/type_checking_stripper.py +25 -0
- pynecore/transformers/unused_series_detector.py +267 -0
- pynecore/types/__init__.py +21 -0
- pynecore/types/alert.py +5 -0
- pynecore/types/barmerge.py +5 -0
- pynecore/types/base.py +39 -0
- pynecore/types/box.py +37 -0
- pynecore/types/chart.py +17 -0
- pynecore/types/color.py +107 -0
- pynecore/types/currency.py +5 -0
- pynecore/types/datetime.py +6 -0
- pynecore/types/display.py +5 -0
- pynecore/types/dividends.py +5 -0
- pynecore/types/earnings.py +5 -0
- pynecore/types/extend.py +5 -0
- pynecore/types/font.py +5 -0
- pynecore/types/footprint.py +41 -0
- pynecore/types/format.py +5 -0
- pynecore/types/hline.py +24 -0
- pynecore/types/ib_persistent.py +8 -0
- pynecore/types/ib_persistent.pyi +10 -0
- pynecore/types/label.py +35 -0
- pynecore/types/line.py +32 -0
- pynecore/types/linefill.py +13 -0
- pynecore/types/location.py +5 -0
- pynecore/types/matrix.py +999 -0
- pynecore/types/na.py +237 -0
- pynecore/types/na.pyi +83 -0
- pynecore/types/ohlcv.py +12 -0
- pynecore/types/order.py +5 -0
- pynecore/types/persistent.py +8 -0
- pynecore/types/persistent.pyi +13 -0
- pynecore/types/pine_types.py +11 -0
- pynecore/types/pine_types.pyi +15 -0
- pynecore/types/pivotpointtype.py +5 -0
- pynecore/types/plot.py +12 -0
- pynecore/types/plot_meta.py +60 -0
- pynecore/types/polyline.py +40 -0
- pynecore/types/position.py +5 -0
- pynecore/types/scale.py +5 -0
- pynecore/types/script_type.py +15 -0
- pynecore/types/series.py +23 -0
- pynecore/types/series.pyi +19 -0
- pynecore/types/session.py +35 -0
- pynecore/types/shape.py +5 -0
- pynecore/types/size.py +5 -0
- pynecore/types/source.py +33 -0
- pynecore/types/splits.py +5 -0
- pynecore/types/strategy.py +45 -0
- pynecore/types/table.py +87 -0
- pynecore/types/text.py +13 -0
- pynecore/types/type_checker.py +7 -0
- pynecore/types/type_checker.pyi +48 -0
- pynecore/types/volume_row.py +36 -0
- pynecore/types/weekdays.py +11 -0
- pynecore/types/xloc.py +5 -0
- pynecore/types/yloc.py +5 -0
- pynecore/utils/__init__.py +0 -0
- pynecore/utils/file_utils.py +50 -0
- pynecore/utils/rich/__init__.py +0 -0
- pynecore/utils/rich/date_column.py +25 -0
- pynecore/utils/sequence_view.py +92 -0
- pynecore/utils/stdlib_checker.py +17 -0
|
@@ -0,0 +1,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,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."""
|