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,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)