deeptrade-quant 0.0.2__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 (83) hide show
  1. deeptrade/__init__.py +8 -0
  2. deeptrade/channels_builtin/__init__.py +0 -0
  3. deeptrade/channels_builtin/stdout/__init__.py +0 -0
  4. deeptrade/channels_builtin/stdout/deeptrade_plugin.yaml +25 -0
  5. deeptrade/channels_builtin/stdout/migrations/20260429_001_init.sql +13 -0
  6. deeptrade/channels_builtin/stdout/stdout_channel/__init__.py +0 -0
  7. deeptrade/channels_builtin/stdout/stdout_channel/channel.py +180 -0
  8. deeptrade/cli.py +214 -0
  9. deeptrade/cli_config.py +396 -0
  10. deeptrade/cli_data.py +33 -0
  11. deeptrade/cli_plugin.py +176 -0
  12. deeptrade/core/__init__.py +8 -0
  13. deeptrade/core/config.py +344 -0
  14. deeptrade/core/config_migrations.py +138 -0
  15. deeptrade/core/db.py +176 -0
  16. deeptrade/core/llm_client.py +591 -0
  17. deeptrade/core/llm_manager.py +174 -0
  18. deeptrade/core/logging_config.py +61 -0
  19. deeptrade/core/migrations/__init__.py +0 -0
  20. deeptrade/core/migrations/core/20260427_001_init.sql +121 -0
  21. deeptrade/core/migrations/core/20260501_002_drop_llm_calls_stage.sql +10 -0
  22. deeptrade/core/migrations/core/__init__.py +0 -0
  23. deeptrade/core/notifier.py +302 -0
  24. deeptrade/core/paths.py +49 -0
  25. deeptrade/core/plugin_manager.py +616 -0
  26. deeptrade/core/run_status.py +29 -0
  27. deeptrade/core/secrets.py +152 -0
  28. deeptrade/core/tushare_client.py +824 -0
  29. deeptrade/plugins_api/__init__.py +44 -0
  30. deeptrade/plugins_api/base.py +66 -0
  31. deeptrade/plugins_api/channel.py +42 -0
  32. deeptrade/plugins_api/events.py +61 -0
  33. deeptrade/plugins_api/llm.py +46 -0
  34. deeptrade/plugins_api/metadata.py +84 -0
  35. deeptrade/plugins_api/notify.py +67 -0
  36. deeptrade/strategies_builtin/__init__.py +0 -0
  37. deeptrade/strategies_builtin/limit_up_board/__init__.py +0 -0
  38. deeptrade/strategies_builtin/limit_up_board/deeptrade_plugin.yaml +101 -0
  39. deeptrade/strategies_builtin/limit_up_board/limit_up_board/__init__.py +0 -0
  40. deeptrade/strategies_builtin/limit_up_board/limit_up_board/calendar.py +65 -0
  41. deeptrade/strategies_builtin/limit_up_board/limit_up_board/cli.py +269 -0
  42. deeptrade/strategies_builtin/limit_up_board/limit_up_board/config.py +76 -0
  43. deeptrade/strategies_builtin/limit_up_board/limit_up_board/data.py +1191 -0
  44. deeptrade/strategies_builtin/limit_up_board/limit_up_board/pipeline.py +869 -0
  45. deeptrade/strategies_builtin/limit_up_board/limit_up_board/plugin.py +30 -0
  46. deeptrade/strategies_builtin/limit_up_board/limit_up_board/profiles.py +85 -0
  47. deeptrade/strategies_builtin/limit_up_board/limit_up_board/prompts.py +485 -0
  48. deeptrade/strategies_builtin/limit_up_board/limit_up_board/render.py +890 -0
  49. deeptrade/strategies_builtin/limit_up_board/limit_up_board/runner.py +1087 -0
  50. deeptrade/strategies_builtin/limit_up_board/limit_up_board/runtime.py +172 -0
  51. deeptrade/strategies_builtin/limit_up_board/limit_up_board/schemas.py +178 -0
  52. deeptrade/strategies_builtin/limit_up_board/migrations/20260430_001_init.sql +150 -0
  53. deeptrade/strategies_builtin/limit_up_board/migrations/20260501_002_lub_stage_results_llm_provider.sql +8 -0
  54. deeptrade/strategies_builtin/limit_up_board/migrations/20260508_001_lub_lhb_tables.sql +36 -0
  55. deeptrade/strategies_builtin/limit_up_board/migrations/20260508_002_lub_cyq_perf.sql +18 -0
  56. deeptrade/strategies_builtin/limit_up_board/migrations/20260508_003_lub_lhb_pk_fix.sql +46 -0
  57. deeptrade/strategies_builtin/limit_up_board/migrations/20260508_004_lub_lhb_drop_pk.sql +53 -0
  58. deeptrade/strategies_builtin/limit_up_board/migrations/20260508_005_lub_config.sql +17 -0
  59. deeptrade/strategies_builtin/volume_anomaly/__init__.py +0 -0
  60. deeptrade/strategies_builtin/volume_anomaly/deeptrade_plugin.yaml +59 -0
  61. deeptrade/strategies_builtin/volume_anomaly/migrations/20260430_001_init.sql +94 -0
  62. deeptrade/strategies_builtin/volume_anomaly/migrations/20260601_001_realized_returns.sql +44 -0
  63. deeptrade/strategies_builtin/volume_anomaly/migrations/20260601_002_dimension_scores.sql +13 -0
  64. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/__init__.py +0 -0
  65. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/calendar.py +52 -0
  66. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/cli.py +247 -0
  67. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/data.py +2154 -0
  68. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/pipeline.py +327 -0
  69. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/plugin.py +22 -0
  70. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/profiles.py +49 -0
  71. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/prompts.py +187 -0
  72. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/prompts_examples.py +84 -0
  73. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/render.py +906 -0
  74. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/runner.py +772 -0
  75. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/runtime.py +90 -0
  76. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/schemas.py +97 -0
  77. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/stats.py +174 -0
  78. deeptrade/theme.py +48 -0
  79. deeptrade_quant-0.0.2.dist-info/METADATA +166 -0
  80. deeptrade_quant-0.0.2.dist-info/RECORD +83 -0
  81. deeptrade_quant-0.0.2.dist-info/WHEEL +4 -0
  82. deeptrade_quant-0.0.2.dist-info/entry_points.txt +2 -0
  83. deeptrade_quant-0.0.2.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,396 @@
1
+ """`deeptrade config` subcommand group.
2
+
3
+ v0.6 — multi-provider LLM (DESIGN §0.7 / §10):
4
+
5
+ * ``set-deepseek`` removed; replaced by ``set-llm`` (interactive new /
6
+ edit / delete) + ``list-llm``.
7
+ * ``config test`` replaced by ``test-llm [name]`` (provider-targeted).
8
+ * ``show`` expands ``llm.providers`` so each provider's api_key gets its
9
+ own masked row.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import TYPE_CHECKING
15
+
16
+ import questionary
17
+ import typer
18
+ from rich.console import Console
19
+ from rich.table import Table
20
+
21
+ from deeptrade.core import paths
22
+ from deeptrade.core.config import (
23
+ ConfigService,
24
+ known_keys,
25
+ )
26
+ from deeptrade.core.db import Database
27
+
28
+ if TYPE_CHECKING: # pragma: no cover
29
+ pass
30
+
31
+ app = typer.Typer(help="View / edit configuration", no_args_is_help=True)
32
+
33
+
34
+ # ---------------------------------------------------------------------------
35
+ # Helpers
36
+ # ---------------------------------------------------------------------------
37
+
38
+
39
+ def _open_service() -> tuple[Database, ConfigService]:
40
+ """Open the local DuckDB + ConfigService. Caller must close db."""
41
+ db = Database(paths.db_path())
42
+ return db, ConfigService(db)
43
+
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # show
47
+ # ---------------------------------------------------------------------------
48
+
49
+
50
+ @app.command("show")
51
+ def cmd_show() -> None:
52
+ """List all known config keys with values (secrets masked) and source."""
53
+ db, svc = _open_service()
54
+ try:
55
+ console = Console()
56
+ table = Table(title="DeepTrade Configuration")
57
+ table.add_column("Key", style="cyan")
58
+ table.add_column("Value", overflow="fold")
59
+ table.add_column("Source", style="yellow")
60
+ for key, value, source in svc.list_all():
61
+ display = "" if value is None else str(value)
62
+ table.add_row(key, display, source)
63
+ console.print(table)
64
+ finally:
65
+ db.close()
66
+
67
+
68
+ # ---------------------------------------------------------------------------
69
+ # set
70
+ # ---------------------------------------------------------------------------
71
+
72
+
73
+ @app.command("set")
74
+ def cmd_set(
75
+ key: str = typer.Argument(..., help="Dotted key (e.g. `app.profile`)"),
76
+ value: str = typer.Argument(..., help="Value (string-coerced)"),
77
+ ) -> None:
78
+ """Set a single config key to a value (scriptable form).
79
+
80
+ For multi-provider LLM keys (``llm.<name>.*``), prefer
81
+ ``deeptrade config set-llm`` — it walks you through the full provider
82
+ record interactively.
83
+ """
84
+ db, svc = _open_service()
85
+ try:
86
+ from deeptrade.core.config import is_secret_key # noqa: PLC0415
87
+
88
+ if key not in known_keys() and not is_secret_key(key):
89
+ typer.echo(f"Unknown key: {key!r}; valid keys:\n " + "\n ".join(known_keys()))
90
+ raise typer.Exit(2)
91
+ try:
92
+ svc.set(key, value)
93
+ except ValueError as e:
94
+ typer.echo(f"Invalid value for {key!r}: {e}")
95
+ raise typer.Exit(2) from e
96
+ typer.echo(f"✔ Saved {key}")
97
+ finally:
98
+ db.close()
99
+
100
+
101
+ @app.command("set-tushare")
102
+ def cmd_set_tushare() -> None:
103
+ """Interactive Tushare configuration."""
104
+ db, svc = _open_service()
105
+ try:
106
+ cur = svc.get_app_config()
107
+ token = questionary.password("Tushare token:").ask()
108
+ if token is None:
109
+ raise typer.Exit(1)
110
+ rps_input = questionary.text(
111
+ f"Tushare RPS [{cur.tushare_rps}]:",
112
+ default=str(cur.tushare_rps),
113
+ ).ask()
114
+ timeout_input = questionary.text(
115
+ f"Tushare timeout (s) [{cur.tushare_timeout}]:",
116
+ default=str(cur.tushare_timeout),
117
+ ).ask()
118
+ if token:
119
+ svc.set("tushare.token", token)
120
+ try:
121
+ svc.set("tushare.rps", float(rps_input))
122
+ svc.set("tushare.timeout", int(timeout_input))
123
+ except (ValueError, TypeError) as e:
124
+ typer.echo(f"Invalid number: {e}")
125
+ raise typer.Exit(2) from e
126
+ typer.echo("✔ Saved tushare config")
127
+ finally:
128
+ db.close()
129
+
130
+
131
+ # ---------------------------------------------------------------------------
132
+ # LLM provider management (v0.6)
133
+ # ---------------------------------------------------------------------------
134
+
135
+
136
+ _DEFAULT_BASE_URLS: dict[str, str] = {
137
+ "deepseek": "https://api.deepseek.com",
138
+ "qwen": "https://dashscope.aliyuncs.com/compatible-mode/v1",
139
+ "kimi": "https://api.moonshot.cn/v1",
140
+ "doubao": "https://ark.cn-beijing.volces.com/api/v3",
141
+ }
142
+
143
+
144
+ @app.command("set-llm")
145
+ def cmd_set_llm() -> None:
146
+ """Interactive LLM provider management (new / edit / delete).
147
+
148
+ Walks the user through the full provider record (name + api_key +
149
+ base_url + model + timeout). Each provider stored in ``llm.providers``
150
+ is independently usable; plugins pick by name via
151
+ ``LLMManager.get_client(name=...)``.
152
+ """
153
+ db, svc = _open_service()
154
+ try:
155
+ cfg = svc.get_app_config()
156
+ existing = sorted(cfg.llm_providers.keys())
157
+
158
+ if existing:
159
+ choices = ["[+] Add new provider"] + [f"[~] {n}" for n in existing] + ["[x] Delete a provider"]
160
+ picked = questionary.select(
161
+ "Pick action:", choices=choices
162
+ ).ask()
163
+ if picked is None:
164
+ raise typer.Exit(1)
165
+ if picked.startswith("[+]"):
166
+ _set_llm_new(svc)
167
+ elif picked.startswith("[x]"):
168
+ _set_llm_delete(svc, existing)
169
+ else:
170
+ name = picked[4:] # strip "[~] "
171
+ _set_llm_edit(svc, name)
172
+ else:
173
+ typer.echo("No LLM providers configured yet — let's add the first one.")
174
+ _set_llm_new(svc)
175
+ finally:
176
+ db.close()
177
+
178
+
179
+ def _set_llm_new(svc: ConfigService) -> None:
180
+ name = questionary.text(
181
+ "Provider name (e.g. deepseek, qwen-plus, kimi):"
182
+ ).ask()
183
+ if not name:
184
+ raise typer.Exit(1)
185
+ name = name.strip()
186
+ if "." in name:
187
+ typer.echo(f"Invalid provider name: {name!r} (must not contain '.')")
188
+ raise typer.Exit(2)
189
+ cfg = svc.get_app_config()
190
+ if name in cfg.llm_providers:
191
+ typer.echo(f"Provider {name!r} already exists; pick edit instead.")
192
+ raise typer.Exit(2)
193
+ default_base = _DEFAULT_BASE_URLS.get(name.split("-")[0], "")
194
+ _prompt_and_save_provider(svc, name, defaults=None, default_base_url=default_base)
195
+
196
+
197
+ def _set_llm_edit(svc: ConfigService, name: str) -> None:
198
+ cfg = svc.get_app_config()
199
+ cur = cfg.llm_providers[name]
200
+ _prompt_and_save_provider(svc, name, defaults=cur.model_dump(), default_base_url=cur.base_url)
201
+
202
+
203
+ def _prompt_and_save_provider(
204
+ svc: ConfigService,
205
+ name: str,
206
+ *,
207
+ defaults: dict | None,
208
+ default_base_url: str,
209
+ ) -> None:
210
+ base_url_default = defaults.get("base_url", default_base_url) if defaults else default_base_url
211
+ model_default = defaults.get("model", "") if defaults else ""
212
+ timeout_default = defaults.get("timeout", 180) if defaults else 180
213
+
214
+ base_url = questionary.text(
215
+ f"Base URL [{base_url_default}]:",
216
+ default=base_url_default,
217
+ ).ask()
218
+ if not base_url:
219
+ raise typer.Exit(1)
220
+ model = questionary.text(
221
+ f"Model name [{model_default}]:",
222
+ default=model_default,
223
+ ).ask()
224
+ if not model:
225
+ raise typer.Exit(1)
226
+ timeout_input = questionary.text(
227
+ f"Timeout (s) [{timeout_default}]:",
228
+ default=str(timeout_default),
229
+ ).ask()
230
+ if timeout_input is None:
231
+ raise typer.Exit(1)
232
+ try:
233
+ timeout = int(timeout_input)
234
+ except ValueError as e:
235
+ typer.echo(f"Invalid timeout: {e}")
236
+ raise typer.Exit(2) from e
237
+
238
+ api_key_prompt = (
239
+ "API key (leave empty to keep existing):"
240
+ if defaults is not None
241
+ else "API key:"
242
+ )
243
+ api_key = questionary.password(api_key_prompt).ask()
244
+ if api_key is None:
245
+ raise typer.Exit(1)
246
+
247
+ try:
248
+ svc.set_llm_provider(
249
+ name,
250
+ base_url=base_url,
251
+ model=model,
252
+ timeout=timeout,
253
+ api_key=api_key if api_key else None,
254
+ )
255
+ except ValueError as e:
256
+ typer.echo(f"Invalid provider: {e}")
257
+ raise typer.Exit(2) from e
258
+
259
+ if defaults is None and not api_key:
260
+ typer.echo(
261
+ f"⚠ Saved provider {name!r} but no api_key was set — it won't appear "
262
+ "in `list-llm` until you run set-llm again to add the key."
263
+ )
264
+ else:
265
+ typer.echo(f"✔ Saved LLM provider {name!r}")
266
+
267
+
268
+ def _set_llm_delete(svc: ConfigService, existing: list[str]) -> None:
269
+ name = questionary.select("Pick provider to delete:", choices=existing).ask()
270
+ if name is None:
271
+ raise typer.Exit(1)
272
+ confirm = questionary.confirm(
273
+ f"Delete provider {name!r} (and its api_key)?", default=False
274
+ ).ask()
275
+ if not confirm:
276
+ raise typer.Exit(1)
277
+ svc.delete_llm_provider(name)
278
+ typer.echo(f"✔ Deleted LLM provider {name!r}")
279
+
280
+
281
+ @app.command("list-llm")
282
+ def cmd_list_llm() -> None:
283
+ """List all configured LLM providers (those with ``api_key`` set).
284
+
285
+ Mirrors ``LLMManager.list_providers()`` — what plugins will see.
286
+ """
287
+ db, _svc = _open_service()
288
+ try:
289
+ from deeptrade.core.config import ConfigService # noqa: PLC0415
290
+ from deeptrade.core.llm_manager import LLMManager # noqa: PLC0415
291
+
292
+ cfg = ConfigService(db)
293
+ mgr = LLMManager(db, cfg)
294
+ names = mgr.list_providers()
295
+ if not names:
296
+ typer.echo("(no LLM providers configured; run `deeptrade config set-llm`)")
297
+ return
298
+
299
+ console = Console()
300
+ table = Table(title="LLM Providers")
301
+ table.add_column("Name", style="cyan")
302
+ table.add_column("Model", overflow="fold")
303
+ table.add_column("Base URL", overflow="fold")
304
+ for name in names:
305
+ info = mgr.get_provider_info(name)
306
+ table.add_row(info.name, info.model, info.base_url)
307
+ console.print(table)
308
+ finally:
309
+ db.close()
310
+
311
+
312
+ # ---------------------------------------------------------------------------
313
+ # test-llm
314
+ # ---------------------------------------------------------------------------
315
+
316
+
317
+ @app.command("test-llm")
318
+ def cmd_test_llm(
319
+ name: str | None = typer.Argument(
320
+ None,
321
+ help="Provider name to test; omit to test every configured provider.",
322
+ ),
323
+ ) -> None:
324
+ """Connectivity check via the production ``LLMClient`` for one or all providers.
325
+
326
+ Each test sends a tiny JSON-mode echo through the cheapest stage profile
327
+ (``final_ranking``) so the no-tools / JSON-mode constraints are exercised.
328
+ """
329
+ db, _svc = _open_service()
330
+ try:
331
+ from deeptrade.core.config import ConfigService # noqa: PLC0415
332
+ from deeptrade.core.llm_manager import LLMManager # noqa: PLC0415
333
+
334
+ cfg = ConfigService(db)
335
+ mgr = LLMManager(db, cfg)
336
+
337
+ if name is not None:
338
+ targets = [name]
339
+ else:
340
+ targets = mgr.list_providers()
341
+ if not targets:
342
+ typer.echo("(no LLM providers configured; run `deeptrade config set-llm`)")
343
+ raise typer.Exit(1)
344
+
345
+ any_failed = False
346
+ for target in targets:
347
+ ok, msg = _test_one_llm(mgr, target)
348
+ marker = "✔" if ok else "✘"
349
+ typer.echo(f"{marker} LLM[{target}]: {msg}")
350
+ if not ok:
351
+ any_failed = True
352
+ if any_failed:
353
+ raise typer.Exit(1)
354
+ finally:
355
+ db.close()
356
+
357
+
358
+ def _test_one_llm(mgr, target: str) -> tuple[bool, str]: # type: ignore[no-untyped-def]
359
+ """Echo a 1-token JSON via a minimal StageProfile through the production client."""
360
+ import time as _time # noqa: PLC0415
361
+
362
+ from pydantic import BaseModel, ConfigDict # noqa: PLC0415
363
+
364
+ from deeptrade.core.llm_manager import LLMNotConfiguredError # noqa: PLC0415
365
+ from deeptrade.plugins_api import StageProfile # noqa: PLC0415
366
+
367
+ class _Echo(BaseModel):
368
+ model_config = ConfigDict(extra="forbid")
369
+ ok: bool
370
+
371
+ try:
372
+ client = mgr.get_client(target, plugin_id="__framework__", run_id=None)
373
+ except LLMNotConfiguredError as e:
374
+ return False, str(e)
375
+
376
+ # v0.7: framework owns no profile presets; supply a minimal echo-friendly
377
+ # profile directly. thinking off + tiny output cap keeps the test cheap.
378
+ echo_profile = StageProfile(
379
+ thinking=False, reasoning_effort="low", temperature=0.0, max_output_tokens=1024
380
+ )
381
+ try:
382
+ t0 = _time.time()
383
+ client.complete_json(
384
+ system='Reply ONLY with this JSON: {"ok": true}',
385
+ user="ping",
386
+ schema=_Echo,
387
+ profile=echo_profile,
388
+ )
389
+ latency_ms = int((_time.time() - t0) * 1000)
390
+ return True, f"echo ok ({latency_ms}ms)"
391
+ except Exception as e: # noqa: BLE001
392
+ return False, f"{type(e).__name__}: {e}"
393
+
394
+
395
+ # Note: cmd_set passes raw strings; Pydantic field validators in AppConfig
396
+ # coerce strings → int / float / time as needed.
deeptrade/cli_data.py ADDED
@@ -0,0 +1,33 @@
1
+ """`deeptrade data` subcommand group.
2
+
3
+ Currently a placeholder: the previous implementation depended on framework
4
+ assets (StrategyContext / StrategyParams / StrategyRunner) that were removed
5
+ in the v0.5 framework reshape. The ``data sync`` capability will be restored
6
+ in the next iteration as part of the per-plugin data ownership model
7
+ (see docs/plugin_cli_dispatch_evaluation.md §6).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import typer
13
+
14
+ app = typer.Typer(
15
+ help="Sync data for a plugin without running its main pipeline.",
16
+ no_args_is_help=True,
17
+ )
18
+
19
+
20
+ @app.command("sync")
21
+ def cmd_sync() -> None:
22
+ """Stub — temporarily disabled while the data layer is being refactored.
23
+
24
+ Run the plugin's own sync command directly, e.g.
25
+ ``deeptrade <plugin_id> sync ...``.
26
+ """
27
+ typer.echo(
28
+ "✘ `deeptrade data sync` is temporarily disabled while the data layer "
29
+ "is being refactored.\n"
30
+ " Use the plugin's own sync command instead, e.g. "
31
+ "`deeptrade <plugin_id> sync ...`."
32
+ )
33
+ raise typer.Exit(2)
@@ -0,0 +1,176 @@
1
+ """`deeptrade plugin` subcommand group."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import questionary
8
+ import typer
9
+ import yaml
10
+ from rich.console import Console
11
+ from rich.table import Table
12
+
13
+ from deeptrade.core import paths
14
+ from deeptrade.core.db import Database
15
+ from deeptrade.core.plugin_manager import (
16
+ PluginInstallError,
17
+ PluginManager,
18
+ PluginNotFoundError,
19
+ _load_metadata_yaml,
20
+ summarize_for_install,
21
+ )
22
+
23
+ app = typer.Typer(help="Install / manage plugins", no_args_is_help=True)
24
+
25
+
26
+ def _open() -> tuple[Database, PluginManager]:
27
+ db = Database(paths.db_path())
28
+ return db, PluginManager(db)
29
+
30
+
31
+ @app.command("install")
32
+ def cmd_install(
33
+ path: Path = typer.Argument(..., help="Local path to the plugin source directory"),
34
+ yes: bool = typer.Option(False, "-y", "--yes", help="Skip the confirmation prompt"),
35
+ ) -> None:
36
+ """Install a plugin from a local directory (no network)."""
37
+ if not path.is_dir():
38
+ typer.echo(f"Not a directory: {path}")
39
+ raise typer.Exit(2)
40
+
41
+ # Load + validate metadata before any DB access
42
+ try:
43
+ meta = _load_metadata_yaml(path / "deeptrade_plugin.yaml")
44
+ except PluginInstallError as e:
45
+ typer.echo(f"✘ {e}")
46
+ raise typer.Exit(2) from e
47
+
48
+ typer.echo("─── 即将安装 ─────────────────────────────")
49
+ typer.echo(summarize_for_install(meta, path))
50
+ typer.echo("──────────────────────────────────────────")
51
+ if not yes:
52
+ ok = questionary.confirm("确认安装?", default=False).ask()
53
+ if not ok:
54
+ typer.echo("Aborted.")
55
+ raise typer.Exit(1)
56
+
57
+ db, mgr = _open()
58
+ try:
59
+ rec = mgr.install(path)
60
+ except PluginInstallError as e:
61
+ typer.echo(f"✘ Install failed: {e}")
62
+ raise typer.Exit(2) from e
63
+ finally:
64
+ db.close()
65
+
66
+ typer.echo(f"✔ 已安装: {rec.plugin_id} v{rec.version}")
67
+
68
+
69
+ @app.command("list")
70
+ def cmd_list() -> None:
71
+ """List installed plugins."""
72
+ db, mgr = _open()
73
+ try:
74
+ records = mgr.list_all()
75
+ finally:
76
+ db.close()
77
+
78
+ console = Console()
79
+ table = Table(title="Installed Plugins")
80
+ table.add_column("plugin_id", style="cyan")
81
+ table.add_column("name")
82
+ table.add_column("version")
83
+ table.add_column("enabled", style="green")
84
+ if not records:
85
+ typer.echo("(no plugins installed)")
86
+ return
87
+ for r in records:
88
+ table.add_row(r.plugin_id, r.name, r.version, "yes" if r.enabled else "no")
89
+ console.print(table)
90
+
91
+
92
+ @app.command("info")
93
+ def cmd_info(plugin_id: str = typer.Argument(...)) -> None:
94
+ """Show metadata + installed tables for a plugin."""
95
+ db, mgr = _open()
96
+ try:
97
+ try:
98
+ rec = mgr.info(plugin_id)
99
+ except PluginNotFoundError as e:
100
+ typer.echo(f"✘ {plugin_id} not installed")
101
+ raise typer.Exit(2) from e
102
+ typer.echo(yaml.safe_dump(rec.metadata.model_dump(mode="json"), allow_unicode=True))
103
+ finally:
104
+ db.close()
105
+
106
+
107
+ @app.command("disable")
108
+ def cmd_disable(plugin_id: str = typer.Argument(...)) -> None:
109
+ db, mgr = _open()
110
+ try:
111
+ try:
112
+ mgr.disable(plugin_id)
113
+ except PluginNotFoundError as e:
114
+ typer.echo(f"✘ {plugin_id} not installed")
115
+ raise typer.Exit(2) from e
116
+ typer.echo(f"✔ disabled: {plugin_id}")
117
+ finally:
118
+ db.close()
119
+
120
+
121
+ @app.command("enable")
122
+ def cmd_enable(plugin_id: str = typer.Argument(...)) -> None:
123
+ db, mgr = _open()
124
+ try:
125
+ try:
126
+ mgr.enable(plugin_id)
127
+ except PluginNotFoundError as e:
128
+ typer.echo(f"✘ {plugin_id} not installed")
129
+ raise typer.Exit(2) from e
130
+ typer.echo(f"✔ enabled: {plugin_id}")
131
+ finally:
132
+ db.close()
133
+
134
+
135
+ @app.command("uninstall")
136
+ def cmd_uninstall(
137
+ plugin_id: str = typer.Argument(...),
138
+ purge: bool = typer.Option(False, "--purge", help="DROP plugin tables and forget all data"),
139
+ yes: bool = typer.Option(False, "-y", "--yes", help="Skip confirmation"),
140
+ ) -> None:
141
+ """Uninstall a plugin. Default: keep tables (just disable). --purge: drop tables."""
142
+ db, mgr = _open()
143
+ try:
144
+ try:
145
+ rec = mgr.info(plugin_id)
146
+ except PluginNotFoundError as e:
147
+ typer.echo(f"✘ {plugin_id} not installed")
148
+ raise typer.Exit(2) from e
149
+
150
+ if purge and not yes:
151
+ tables = [t.name for t in rec.metadata.tables if t.purge_on_uninstall]
152
+ typer.echo(f"将删除以下表(不可恢复): {tables}")
153
+ ok = questionary.confirm("确认 --purge?", default=False).ask()
154
+ if not ok:
155
+ typer.echo("Aborted.")
156
+ raise typer.Exit(1)
157
+
158
+ result = mgr.uninstall(plugin_id, purge=purge)
159
+ action = "purged" if purge else "disabled"
160
+ typer.echo(f"✔ {action}: {plugin_id} (dropped tables: {result['purged_tables']})")
161
+ finally:
162
+ db.close()
163
+
164
+
165
+ @app.command("upgrade")
166
+ def cmd_upgrade(path: Path = typer.Argument(...)) -> None:
167
+ db, mgr = _open()
168
+ try:
169
+ try:
170
+ rec = mgr.upgrade(path)
171
+ except (PluginInstallError, PluginNotFoundError) as e:
172
+ typer.echo(f"✘ Upgrade failed: {e}")
173
+ raise typer.Exit(2) from e
174
+ typer.echo(f"✔ upgraded: {rec.plugin_id} → v{rec.version}")
175
+ finally:
176
+ db.close()
@@ -0,0 +1,8 @@
1
+ """Core services layer.
2
+
3
+ Houses cross-cutting infrastructure (DB, config, secrets, clients, notifier)
4
+ that plugins consume directly via the public ``deeptrade.plugins_api`` surface
5
+ and the top-level ``deeptrade.notify`` / ``deeptrade.notification_session`` API.
6
+ """
7
+
8
+ from __future__ import annotations