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,343 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
from typer.core import TyperGroup
|
|
6
|
+
|
|
7
|
+
from ..app import app, app_state
|
|
8
|
+
from ..pluggable import PluggableCommand
|
|
9
|
+
from ..utils.error_hook import setup_global_error_logging
|
|
10
|
+
|
|
11
|
+
# Import commands
|
|
12
|
+
from . import run, data, compile, benchmark, debug, plugin, optimize
|
|
13
|
+
|
|
14
|
+
__all__ = ['run', 'data', 'compile', 'benchmark', 'debug', 'plugin', 'optimize']
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.callback()
|
|
20
|
+
def setup(
|
|
21
|
+
ctx: typer.Context,
|
|
22
|
+
workdir: Path = typer.Option(
|
|
23
|
+
app_state.workdir,
|
|
24
|
+
"--workdir", "-w",
|
|
25
|
+
envvar="PYNE_WORK_DIR",
|
|
26
|
+
help="Working directory",
|
|
27
|
+
file_okay=False, dir_okay=True,
|
|
28
|
+
resolve_path=True,
|
|
29
|
+
),
|
|
30
|
+
recreate_demo: bool = typer.Option(
|
|
31
|
+
False,
|
|
32
|
+
"--recreate-demo",
|
|
33
|
+
help="Recreate demo.py and demo.ohlcv files even if workdir exists",
|
|
34
|
+
),
|
|
35
|
+
recreate_provider_config: bool = typer.Option(
|
|
36
|
+
False,
|
|
37
|
+
"--recreate-provider-config",
|
|
38
|
+
help="Recreate provider.toml file even if workdir exists",
|
|
39
|
+
),
|
|
40
|
+
recreate_api_config: bool = typer.Option(
|
|
41
|
+
False,
|
|
42
|
+
"--recreate-api-config",
|
|
43
|
+
help="Recreate api.toml file even if workdir exists",
|
|
44
|
+
)
|
|
45
|
+
):
|
|
46
|
+
"""
|
|
47
|
+
Pyne Command Line Interface
|
|
48
|
+
"""
|
|
49
|
+
if ctx.resilient_parsing:
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
# If no subcommand is provided, show complete help like --help
|
|
53
|
+
if ctx.invoked_subcommand is None:
|
|
54
|
+
typer.echo(ctx.get_help())
|
|
55
|
+
raise typer.Exit(0)
|
|
56
|
+
|
|
57
|
+
typer.echo("")
|
|
58
|
+
|
|
59
|
+
# Check if workdir is available
|
|
60
|
+
workdir_existed = Path(workdir).exists()
|
|
61
|
+
if not workdir_existed:
|
|
62
|
+
typer.echo(f"Working directory '{workdir}' does not exist.")
|
|
63
|
+
typer.confirm("Do you want to create it?", abort=True)
|
|
64
|
+
|
|
65
|
+
# Create workdir
|
|
66
|
+
Path(workdir).mkdir(parents=True, exist_ok=False)
|
|
67
|
+
|
|
68
|
+
# Create the .pyne marker file so the directory is recognized as a workdir by content
|
|
69
|
+
marker_file = Path(workdir) / '.pyne'
|
|
70
|
+
if not marker_file.exists():
|
|
71
|
+
marker_file.write_text(
|
|
72
|
+
"# PyneCore workdir marker. The version refers to the workdir structure, "
|
|
73
|
+
"not your project.\n"
|
|
74
|
+
'workdir_version = "1.0"\n'
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Create scripts directory
|
|
78
|
+
scripts_dir = Path(workdir) / 'scripts' / 'lib'
|
|
79
|
+
scripts_dir.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
|
|
81
|
+
# Create demo.py file only if we created the workdir in this run or recreate_demo is True
|
|
82
|
+
if not workdir_existed or recreate_demo:
|
|
83
|
+
demo_file = Path(workdir) / 'scripts' / 'demo.py'
|
|
84
|
+
if not demo_file.exists() or recreate_demo:
|
|
85
|
+
if recreate_demo and demo_file.exists():
|
|
86
|
+
typer.echo("Recreating demo.py...")
|
|
87
|
+
with demo_file.open('w') as f:
|
|
88
|
+
f.write('''"""
|
|
89
|
+
@pyne
|
|
90
|
+
Simple Pyne code Demo
|
|
91
|
+
|
|
92
|
+
A basic demo showing a 12 and 26 period EMA crossover system.
|
|
93
|
+
"""
|
|
94
|
+
from pynecore import Series
|
|
95
|
+
from pynecore.lib import script, input, plot, color, ta
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@script.indicator(
|
|
99
|
+
title="Simple EMA Crossover Demo",
|
|
100
|
+
shorttitle="EMA Demo",
|
|
101
|
+
overlay=True
|
|
102
|
+
)
|
|
103
|
+
def main(
|
|
104
|
+
src: Series[float] = input.source("close", title="Price Source"),
|
|
105
|
+
fast_length: int = input.int(12, title="Fast EMA Length"),
|
|
106
|
+
slow_length: int = input.int(26, title="Slow EMA Length")
|
|
107
|
+
):
|
|
108
|
+
"""
|
|
109
|
+
A simple EMA crossover demo
|
|
110
|
+
"""
|
|
111
|
+
# Calculate EMAs
|
|
112
|
+
fast_ema = ta.ema(src, fast_length)
|
|
113
|
+
slow_ema = ta.ema(src, slow_length)
|
|
114
|
+
|
|
115
|
+
# Plot our indicators
|
|
116
|
+
plot(fast_ema, title="Fast EMA", color=color.blue)
|
|
117
|
+
plot(slow_ema, title="Slow EMA", color=color.red)
|
|
118
|
+
''')
|
|
119
|
+
|
|
120
|
+
# Create data directory
|
|
121
|
+
data_dir = Path(workdir) / 'data'
|
|
122
|
+
data_dir.mkdir(exist_ok=True)
|
|
123
|
+
|
|
124
|
+
# Create demo.ohlcv file only if we created the workdir in this run or recreate_demo is True
|
|
125
|
+
if not workdir_existed or recreate_demo:
|
|
126
|
+
from datetime import datetime, timedelta, time as dt_time
|
|
127
|
+
from ...core.ohlcv_file import OHLCVWriter
|
|
128
|
+
from ...core.syminfo import SymInfo, SymInfoInterval, SymInfoSession
|
|
129
|
+
from ...types.ohlcv import OHLCV
|
|
130
|
+
import random
|
|
131
|
+
|
|
132
|
+
demo_file = data_dir / 'demo.ohlcv'
|
|
133
|
+
if not demo_file.exists() or recreate_demo:
|
|
134
|
+
if recreate_demo and demo_file.exists():
|
|
135
|
+
typer.echo("Recreating demo.ohlcv and demo.toml...")
|
|
136
|
+
# Remove existing files to start fresh
|
|
137
|
+
demo_file.unlink(missing_ok=True)
|
|
138
|
+
toml_path = demo_file.with_suffix(".toml")
|
|
139
|
+
toml_path.unlink(missing_ok=True)
|
|
140
|
+
# Create opening hours, session starts and ends for 24/7 trading
|
|
141
|
+
opening_hours = []
|
|
142
|
+
session_starts = []
|
|
143
|
+
session_ends = []
|
|
144
|
+
|
|
145
|
+
for day in range(7): # 0 = Monday, 6 = Sunday
|
|
146
|
+
opening_hours.append(SymInfoInterval(
|
|
147
|
+
day=day,
|
|
148
|
+
start=dt_time(0, 0, 0),
|
|
149
|
+
end=dt_time(23, 59, 59)
|
|
150
|
+
))
|
|
151
|
+
session_starts.append(SymInfoSession(
|
|
152
|
+
day=day,
|
|
153
|
+
time=dt_time(0, 0, 0)
|
|
154
|
+
))
|
|
155
|
+
session_ends.append(SymInfoSession(
|
|
156
|
+
day=day,
|
|
157
|
+
time=dt_time(23, 59, 59)
|
|
158
|
+
))
|
|
159
|
+
|
|
160
|
+
# Create symbol info
|
|
161
|
+
syminfo = SymInfo(
|
|
162
|
+
prefix="DEMO",
|
|
163
|
+
description="Demo Benchmark Asset",
|
|
164
|
+
ticker="DEMOBTC",
|
|
165
|
+
currency="BTC",
|
|
166
|
+
basecurrency="DEMO",
|
|
167
|
+
period="1D",
|
|
168
|
+
type="crypto",
|
|
169
|
+
volumetype="base",
|
|
170
|
+
mintick=0.01,
|
|
171
|
+
pricescale=100,
|
|
172
|
+
minmove=1,
|
|
173
|
+
pointvalue=1.0,
|
|
174
|
+
mincontract=0.0001,
|
|
175
|
+
opening_hours=opening_hours,
|
|
176
|
+
session_starts=session_starts,
|
|
177
|
+
session_ends=session_ends,
|
|
178
|
+
timezone="UTC",
|
|
179
|
+
avg_spread=None,
|
|
180
|
+
taker_fee=0.001,
|
|
181
|
+
maker_fee=0.0005
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Save symbol info
|
|
185
|
+
toml_path = demo_file.with_suffix(".toml")
|
|
186
|
+
syminfo.save_toml(toml_path)
|
|
187
|
+
|
|
188
|
+
# Generate synthetic OHLCV data (2000 candles) with random walk
|
|
189
|
+
start_time = datetime(2020, 1, 1, 0, 0, 0)
|
|
190
|
+
base_price = 100.0
|
|
191
|
+
|
|
192
|
+
# Use fixed seed for reproducibility
|
|
193
|
+
random.seed(42)
|
|
194
|
+
|
|
195
|
+
with OHLCVWriter(demo_file) as writer:
|
|
196
|
+
current_price = base_price
|
|
197
|
+
|
|
198
|
+
for i in range(2000):
|
|
199
|
+
timestamp = int((start_time + timedelta(days=i)).timestamp())
|
|
200
|
+
|
|
201
|
+
# Random walk with slight upward bias
|
|
202
|
+
change_percent = random.gauss(0.0002, 0.02) # 0.02% mean, 2% std dev
|
|
203
|
+
current_price *= (1 + change_percent)
|
|
204
|
+
|
|
205
|
+
# Generate OHLC with some volatility
|
|
206
|
+
daily_volatility = random.uniform(0.01, 0.03)
|
|
207
|
+
|
|
208
|
+
open_price = current_price
|
|
209
|
+
high_price = open_price * (1 + daily_volatility * random.random())
|
|
210
|
+
low_price = open_price * (1 - daily_volatility * random.random())
|
|
211
|
+
close_price = low_price + (high_price - low_price) * random.random()
|
|
212
|
+
|
|
213
|
+
# Update current price for next candle
|
|
214
|
+
current_price = close_price
|
|
215
|
+
|
|
216
|
+
# Volume with some randomness
|
|
217
|
+
volume = 1000000 * (1 + random.uniform(-0.5, 1.0))
|
|
218
|
+
|
|
219
|
+
ohlcv = OHLCV(
|
|
220
|
+
timestamp=timestamp,
|
|
221
|
+
open=open_price,
|
|
222
|
+
high=high_price,
|
|
223
|
+
low=low_price,
|
|
224
|
+
close=close_price,
|
|
225
|
+
volume=volume,
|
|
226
|
+
extra_fields=None
|
|
227
|
+
)
|
|
228
|
+
writer.write(ohlcv)
|
|
229
|
+
# Create output and logs directory
|
|
230
|
+
output_dir = Path(workdir) / 'output' / 'logs'
|
|
231
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
232
|
+
|
|
233
|
+
# Create cache directory (machine-written runtime state: auth sessions, etc.)
|
|
234
|
+
cache_dir = Path(workdir) / 'cache'
|
|
235
|
+
cache_dir.mkdir(exist_ok=True)
|
|
236
|
+
|
|
237
|
+
# Create config directory
|
|
238
|
+
config_dir = Path(workdir) / 'config'
|
|
239
|
+
config_dir.mkdir(exist_ok=True)
|
|
240
|
+
|
|
241
|
+
# Generate per-plugin config files for all installed plugins
|
|
242
|
+
from ...core.plugin import discover_plugins
|
|
243
|
+
from ...core.config import ensure_config
|
|
244
|
+
|
|
245
|
+
plugins_dir = config_dir / 'plugins'
|
|
246
|
+
plugins_dir.mkdir(exist_ok=True)
|
|
247
|
+
|
|
248
|
+
for name, ep in discover_plugins().items():
|
|
249
|
+
config_path = plugins_dir / f'{name}.toml'
|
|
250
|
+
if not config_path.exists() or recreate_provider_config:
|
|
251
|
+
try:
|
|
252
|
+
plugin_cls = ep.load()
|
|
253
|
+
if hasattr(plugin_cls, 'Config') and plugin_cls.Config is not None:
|
|
254
|
+
ensure_config(plugin_cls.Config, config_path)
|
|
255
|
+
except Exception as e:
|
|
256
|
+
logger.warning("Failed to load plugin config '%s': %s", name, e)
|
|
257
|
+
|
|
258
|
+
# Create api.toml file for PyneSys API (if not exists)
|
|
259
|
+
api_file = config_dir / 'api.toml'
|
|
260
|
+
if not api_file.exists() or recreate_api_config:
|
|
261
|
+
with api_file.open('w') as f:
|
|
262
|
+
f.write("""[api]
|
|
263
|
+
api_key = ""
|
|
264
|
+
timeout = 30
|
|
265
|
+
""")
|
|
266
|
+
|
|
267
|
+
# Set workdir in app_state
|
|
268
|
+
app_state.workdir = workdir
|
|
269
|
+
|
|
270
|
+
# Setup global error logging
|
|
271
|
+
setup_global_error_logging(workdir / "output" / "logs" / "error.log")
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
# ---------------------------------------------------------------------------
|
|
275
|
+
# CLIPlugin loading: subcommands and parameter hooks
|
|
276
|
+
# ---------------------------------------------------------------------------
|
|
277
|
+
_BUILTIN_COMMANDS = {'run', 'data', 'compile', 'benchmark', 'debug', 'plugin', 'optimize'}
|
|
278
|
+
# Commands plugins may inject parameters into, as space-separated paths so a
|
|
279
|
+
# nested subcommand (e.g. ``data download``) can be reached as well.
|
|
280
|
+
_PLUGGABLE_COMMANDS = ('run', 'data download')
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _resolve_command(group: TyperGroup, path: str) -> PluggableCommand | None:
|
|
284
|
+
"""Walk a space-separated command path down to a pluggable command."""
|
|
285
|
+
parts = path.split()
|
|
286
|
+
node = group
|
|
287
|
+
for part in parts[:-1]:
|
|
288
|
+
child = node.commands.get(part)
|
|
289
|
+
if not isinstance(child, TyperGroup):
|
|
290
|
+
return None
|
|
291
|
+
node = child
|
|
292
|
+
leaf = node.commands.get(parts[-1])
|
|
293
|
+
return leaf if isinstance(leaf, PluggableCommand) else None
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _register_cli_plugins():
|
|
297
|
+
"""Load CLIPlugin subcommands and parameter hooks from installed plugins."""
|
|
298
|
+
from ...core.plugin import discover_plugins, CLIPlugin
|
|
299
|
+
|
|
300
|
+
for name, ep in discover_plugins().items():
|
|
301
|
+
try:
|
|
302
|
+
plugin_cls = ep.load()
|
|
303
|
+
|
|
304
|
+
if not (isinstance(plugin_cls, type) and issubclass(plugin_cls, CLIPlugin)):
|
|
305
|
+
continue
|
|
306
|
+
|
|
307
|
+
# 1. CLI subcommand registration
|
|
308
|
+
cli_app = plugin_cls.cli()
|
|
309
|
+
if cli_app is not None:
|
|
310
|
+
if name in _BUILTIN_COMMANDS:
|
|
311
|
+
typer.secho(
|
|
312
|
+
f"Warning: plugin '{name}' CLI name conflicts with "
|
|
313
|
+
f"built-in command, skipping",
|
|
314
|
+
fg="yellow", err=True,
|
|
315
|
+
)
|
|
316
|
+
else:
|
|
317
|
+
app.add_typer(cli_app, name=name)
|
|
318
|
+
|
|
319
|
+
# 2. Parameter hook registration
|
|
320
|
+
for cmd_path in _PLUGGABLE_COMMANDS:
|
|
321
|
+
params = plugin_cls.cli_params(cmd_path)
|
|
322
|
+
if not params:
|
|
323
|
+
continue
|
|
324
|
+
|
|
325
|
+
group = typer.main.get_command(app)
|
|
326
|
+
assert isinstance(group, TyperGroup)
|
|
327
|
+
command = _resolve_command(group, cmd_path)
|
|
328
|
+
if command is None:
|
|
329
|
+
continue
|
|
330
|
+
|
|
331
|
+
for param in params:
|
|
332
|
+
if not command.register_plugin_param(param):
|
|
333
|
+
typer.secho(
|
|
334
|
+
f"Warning: plugin '{name}' param '{param.decls[0]}' "
|
|
335
|
+
f"conflicts on '{cmd_path}'",
|
|
336
|
+
fg="yellow", err=True,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
except Exception as e:
|
|
340
|
+
logger.warning("Failed to load CLI plugin '%s': %s", name, e)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
_register_cli_plugins()
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import gc
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import statistics
|
|
5
|
+
|
|
6
|
+
from typer import Option, Argument, secho, Exit
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
|
|
10
|
+
|
|
11
|
+
from ..app import app, app_state
|
|
12
|
+
from ...core.ohlcv_file import OHLCVReader
|
|
13
|
+
from ...core.syminfo import SymInfo
|
|
14
|
+
from ...core.script_runner import ScriptRunner
|
|
15
|
+
|
|
16
|
+
__all__ = []
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.command(name="benchmark")
|
|
20
|
+
def benchmark(
|
|
21
|
+
script: Path = Argument("demo", help="Script to benchmark"),
|
|
22
|
+
data: Path = Argument("demo", help="Data file to use"),
|
|
23
|
+
iterations: int = Option(10, "--iterations", "-i", help="Number of iterations to run"),
|
|
24
|
+
candles: int = Option(5000, "--candles", "-c", help="Number of candles to process"),
|
|
25
|
+
warmup: int = Option(2, "--warmup", "-w", help="Number of warmup iterations"),
|
|
26
|
+
no_output: bool = Option(True, "--no-output", help="Don't write CSV output"),
|
|
27
|
+
):
|
|
28
|
+
"""
|
|
29
|
+
Benchmark script execution performance
|
|
30
|
+
|
|
31
|
+
This command runs a script multiple times to measure performance.
|
|
32
|
+
By default, it uses demo.py and demo.ohlcv files from the workdir.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
console = Console()
|
|
36
|
+
|
|
37
|
+
# Process script path - use same logic as run command
|
|
38
|
+
# Ensure .py extension
|
|
39
|
+
if script.suffix != ".py":
|
|
40
|
+
script = script.with_suffix(".py")
|
|
41
|
+
# Expand script path
|
|
42
|
+
if len(script.parts) == 1:
|
|
43
|
+
script = app_state.scripts_dir / script
|
|
44
|
+
# Check if script exists
|
|
45
|
+
if not script.exists():
|
|
46
|
+
secho(f"Script file '{script}' not found!", fg="red", err=True)
|
|
47
|
+
raise Exit(1)
|
|
48
|
+
|
|
49
|
+
# Process data path - similar logic
|
|
50
|
+
# Check file format and extension
|
|
51
|
+
if data.suffix == "":
|
|
52
|
+
# No extension, add .ohlcv
|
|
53
|
+
data = data.with_suffix(".ohlcv")
|
|
54
|
+
elif data.suffix != ".ohlcv":
|
|
55
|
+
# Has extension but not .ohlcv
|
|
56
|
+
secho(f"Cannot run with '{data.suffix}' files. The PyneCore runtime requires .ohlcv format.",
|
|
57
|
+
fg="red", err=True)
|
|
58
|
+
secho("If you're trying to use a different data format, please convert it first:", fg="red")
|
|
59
|
+
symbol_placeholder = "YOUR_SYMBOL"
|
|
60
|
+
timeframe_placeholder = "YOUR_TIMEFRAME"
|
|
61
|
+
secho(f"pyne data convert-from {data} --symbol {symbol_placeholder} --timeframe {timeframe_placeholder}",
|
|
62
|
+
fg="yellow")
|
|
63
|
+
raise Exit(1)
|
|
64
|
+
|
|
65
|
+
# Expand data path
|
|
66
|
+
if len(data.parts) == 1:
|
|
67
|
+
data = app_state.data_dir / data
|
|
68
|
+
|
|
69
|
+
if not data.exists():
|
|
70
|
+
secho(f"Data file '{data}' not found!", fg="red", err=True)
|
|
71
|
+
raise Exit(1)
|
|
72
|
+
|
|
73
|
+
# Load symbol info
|
|
74
|
+
try:
|
|
75
|
+
syminfo = SymInfo.load_toml(data.with_suffix(".toml"))
|
|
76
|
+
except FileNotFoundError:
|
|
77
|
+
secho(f"Symbol info file '{data.with_suffix('.toml')}' not found!", fg="red", err=True)
|
|
78
|
+
raise Exit(1)
|
|
79
|
+
|
|
80
|
+
secho(f"\nBenchmarking: {script.name}", fg="cyan")
|
|
81
|
+
secho(f"Data: {data.name} ({candles} candles)", fg="cyan")
|
|
82
|
+
secho(f"Iterations: {iterations} (warmup: {warmup})", fg="cyan")
|
|
83
|
+
secho("")
|
|
84
|
+
|
|
85
|
+
# Timing results
|
|
86
|
+
import_times = []
|
|
87
|
+
run_times = []
|
|
88
|
+
total_times = []
|
|
89
|
+
|
|
90
|
+
# Total iterations including warmup
|
|
91
|
+
total_iterations = warmup + iterations
|
|
92
|
+
|
|
93
|
+
with Progress(
|
|
94
|
+
SpinnerColumn(),
|
|
95
|
+
TextColumn("[progress.description]{task.description}"),
|
|
96
|
+
BarColumn(),
|
|
97
|
+
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
|
98
|
+
) as progress:
|
|
99
|
+
task = progress.add_task("Running benchmark...", total=total_iterations)
|
|
100
|
+
|
|
101
|
+
for i in range(total_iterations):
|
|
102
|
+
# Force garbage collection before each run
|
|
103
|
+
gc.collect()
|
|
104
|
+
|
|
105
|
+
# Time the whole process
|
|
106
|
+
total_start = time.perf_counter()
|
|
107
|
+
|
|
108
|
+
# Time the import
|
|
109
|
+
import_start = time.perf_counter()
|
|
110
|
+
|
|
111
|
+
# Open data file and get iterator
|
|
112
|
+
with OHLCVReader(data) as reader:
|
|
113
|
+
start_ts = reader.start_timestamp
|
|
114
|
+
assert start_ts is not None
|
|
115
|
+
# Get only the requested number of candles
|
|
116
|
+
ohlcv_list = []
|
|
117
|
+
# Use read_from to get all data from the beginning
|
|
118
|
+
for idx, ohlcv in enumerate(reader.read_from(start_ts, reader.end_timestamp)):
|
|
119
|
+
if idx >= candles:
|
|
120
|
+
break
|
|
121
|
+
ohlcv_list.append(ohlcv)
|
|
122
|
+
|
|
123
|
+
# Create runner
|
|
124
|
+
runner = ScriptRunner(
|
|
125
|
+
script,
|
|
126
|
+
iter(ohlcv_list),
|
|
127
|
+
syminfo,
|
|
128
|
+
last_bar_index=len(ohlcv_list) - 1,
|
|
129
|
+
plot_path=None if no_output else app_state.output_dir / f"benchmark_{i}.csv"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
import_time = time.perf_counter() - import_start
|
|
133
|
+
|
|
134
|
+
# Time the execution
|
|
135
|
+
run_start = time.perf_counter()
|
|
136
|
+
runner.run()
|
|
137
|
+
run_time = time.perf_counter() - run_start
|
|
138
|
+
|
|
139
|
+
total_time = time.perf_counter() - total_start
|
|
140
|
+
|
|
141
|
+
# Only collect times after warmup
|
|
142
|
+
if i >= warmup:
|
|
143
|
+
import_times.append(import_time)
|
|
144
|
+
run_times.append(run_time)
|
|
145
|
+
total_times.append(total_time)
|
|
146
|
+
|
|
147
|
+
progress.update(task, advance=1)
|
|
148
|
+
|
|
149
|
+
# Clean up output files if no_output
|
|
150
|
+
if no_output and (app_state.output_dir / f"benchmark_{i}.csv").exists():
|
|
151
|
+
(app_state.output_dir / f"benchmark_{i}.csv").unlink()
|
|
152
|
+
|
|
153
|
+
# Calculate statistics
|
|
154
|
+
avg_import = statistics.mean(import_times)
|
|
155
|
+
avg_run = statistics.mean(run_times)
|
|
156
|
+
avg_total = statistics.mean(total_times)
|
|
157
|
+
|
|
158
|
+
min_run = min(run_times)
|
|
159
|
+
max_run = max(run_times)
|
|
160
|
+
std_run = statistics.stdev(run_times) if len(run_times) > 1 else 0
|
|
161
|
+
|
|
162
|
+
# Candles per second
|
|
163
|
+
candles_per_second = candles / avg_run if avg_run > 0 else 0
|
|
164
|
+
|
|
165
|
+
# Create results table
|
|
166
|
+
table = Table(title="Benchmark Results", show_header=True, header_style="bold magenta")
|
|
167
|
+
table.add_column("Metric", style="cyan", no_wrap=True)
|
|
168
|
+
table.add_column("Value", justify="right", style="green")
|
|
169
|
+
|
|
170
|
+
table.add_row("Script", script.name)
|
|
171
|
+
table.add_row("Candles", str(candles))
|
|
172
|
+
table.add_row("Iterations", str(iterations))
|
|
173
|
+
table.add_row("", "")
|
|
174
|
+
table.add_row("Avg Import Time", f"{avg_import * 1000:.2f} ms")
|
|
175
|
+
table.add_row("Avg Run Time", f"{avg_run * 1000:.2f} ms")
|
|
176
|
+
table.add_row("Avg Total Time", f"{avg_total * 1000:.2f} ms")
|
|
177
|
+
table.add_row("", "")
|
|
178
|
+
table.add_row("Min Run Time", f"{min_run * 1000:.2f} ms")
|
|
179
|
+
table.add_row("Max Run Time", f"{max_run * 1000:.2f} ms")
|
|
180
|
+
table.add_row("Std Dev", f"{std_run * 1000:.2f} ms")
|
|
181
|
+
table.add_row("", "")
|
|
182
|
+
table.add_row("Candles/Second", f"{candles_per_second:.0f}")
|
|
183
|
+
table.add_row("Time per Candle", f"{avg_run / candles * 1000:.3f} ms")
|
|
184
|
+
|
|
185
|
+
console.print("")
|
|
186
|
+
console.print(table)
|