deeptrade-quant 0.0.2__tar.gz → 0.2.0__tar.gz

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 (125) hide show
  1. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/.gitignore +3 -0
  2. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/PKG-INFO +24 -7
  3. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/README.md +22 -6
  4. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/__init__.py +1 -1
  5. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/cli.py +33 -6
  6. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/cli_config.py +66 -2
  7. deeptrade_quant-0.2.0/deeptrade/cli_plugin.py +330 -0
  8. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/core/config.py +86 -3
  9. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/core/config_migrations.py +44 -0
  10. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/core/db.py +4 -0
  11. deeptrade_quant-0.2.0/deeptrade/core/github_fetch.py +218 -0
  12. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/core/llm_manager.py +24 -7
  13. deeptrade_quant-0.0.2/deeptrade/core/migrations/core/20260427_001_init.sql → deeptrade_quant-0.2.0/deeptrade/core/migrations/core/20260509_001_init.sql +2 -10
  14. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/core/notifier.py +7 -4
  15. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/core/plugin_manager.py +40 -7
  16. deeptrade_quant-0.2.0/deeptrade/core/plugin_source.py +194 -0
  17. deeptrade_quant-0.2.0/deeptrade/core/registry.py +191 -0
  18. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/pyproject.toml +2 -1
  19. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/tests/cli/test_config_cmd.py +1 -0
  20. deeptrade_quant-0.2.0/tests/cli/test_plugin_cmd.py +320 -0
  21. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/tests/cli/test_routing.py +0 -6
  22. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/tests/core/test_config.py +101 -0
  23. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/tests/core/test_config_migrations.py +57 -0
  24. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/tests/core/test_db.py +3 -8
  25. deeptrade_quant-0.2.0/tests/core/test_github_fetch.py +206 -0
  26. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/tests/core/test_llm_manager.py +33 -0
  27. deeptrade_quant-0.2.0/tests/core/test_plugin_source.py +256 -0
  28. deeptrade_quant-0.2.0/tests/core/test_plugin_upgrade.py +142 -0
  29. deeptrade_quant-0.2.0/tests/core/test_registry.py +261 -0
  30. deeptrade_quant-0.0.2/deeptrade/channels_builtin/stdout/deeptrade_plugin.yaml +0 -25
  31. deeptrade_quant-0.0.2/deeptrade/channels_builtin/stdout/migrations/20260429_001_init.sql +0 -13
  32. deeptrade_quant-0.0.2/deeptrade/channels_builtin/stdout/stdout_channel/channel.py +0 -180
  33. deeptrade_quant-0.0.2/deeptrade/cli_plugin.py +0 -176
  34. deeptrade_quant-0.0.2/deeptrade/core/migrations/core/20260501_002_drop_llm_calls_stage.sql +0 -10
  35. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/__init__.py +0 -0
  36. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/deeptrade_plugin.yaml +0 -101
  37. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/limit_up_board/__init__.py +0 -0
  38. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/limit_up_board/calendar.py +0 -65
  39. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/limit_up_board/cli.py +0 -269
  40. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/limit_up_board/config.py +0 -76
  41. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/limit_up_board/data.py +0 -1191
  42. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/limit_up_board/pipeline.py +0 -869
  43. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/limit_up_board/plugin.py +0 -30
  44. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/limit_up_board/profiles.py +0 -85
  45. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/limit_up_board/prompts.py +0 -485
  46. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/limit_up_board/render.py +0 -890
  47. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/limit_up_board/runner.py +0 -1087
  48. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/limit_up_board/runtime.py +0 -172
  49. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/limit_up_board/schemas.py +0 -178
  50. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/migrations/20260430_001_init.sql +0 -150
  51. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/migrations/20260501_002_lub_stage_results_llm_provider.sql +0 -8
  52. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/migrations/20260508_001_lub_lhb_tables.sql +0 -36
  53. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/migrations/20260508_002_lub_cyq_perf.sql +0 -18
  54. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/migrations/20260508_003_lub_lhb_pk_fix.sql +0 -46
  55. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/migrations/20260508_004_lub_lhb_drop_pk.sql +0 -53
  56. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/migrations/20260508_005_lub_config.sql +0 -17
  57. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/volume_anomaly/__init__.py +0 -0
  58. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/volume_anomaly/deeptrade_plugin.yaml +0 -59
  59. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/volume_anomaly/migrations/20260430_001_init.sql +0 -94
  60. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/volume_anomaly/migrations/20260601_001_realized_returns.sql +0 -44
  61. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/volume_anomaly/migrations/20260601_002_dimension_scores.sql +0 -13
  62. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/__init__.py +0 -0
  63. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/calendar.py +0 -52
  64. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/cli.py +0 -247
  65. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/data.py +0 -2154
  66. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/pipeline.py +0 -327
  67. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/plugin.py +0 -22
  68. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/profiles.py +0 -49
  69. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/prompts.py +0 -187
  70. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/prompts_examples.py +0 -84
  71. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/render.py +0 -906
  72. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/runner.py +0 -772
  73. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/runtime.py +0 -90
  74. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/schemas.py +0 -97
  75. deeptrade_quant-0.0.2/deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/stats.py +0 -174
  76. deeptrade_quant-0.0.2/tests/__init__.py +0 -0
  77. deeptrade_quant-0.0.2/tests/cli/__init__.py +0 -0
  78. deeptrade_quant-0.0.2/tests/core/__init__.py +0 -0
  79. deeptrade_quant-0.0.2/tests/plugins_api/__init__.py +0 -0
  80. deeptrade_quant-0.0.2/tests/strategies_builtin/__init__.py +0 -0
  81. deeptrade_quant-0.0.2/tests/strategies_builtin/limit_up_board/__init__.py +0 -0
  82. deeptrade_quant-0.0.2/tests/strategies_builtin/limit_up_board/test_phase_a_factors.py +0 -250
  83. deeptrade_quant-0.0.2/tests/strategies_builtin/limit_up_board/test_phase_b_factors.py +0 -164
  84. deeptrade_quant-0.0.2/tests/strategies_builtin/limit_up_board/test_v04_settings.py +0 -150
  85. deeptrade_quant-0.0.2/tests/strategies_builtin/volume_anomaly/__init__.py +0 -0
  86. deeptrade_quant-0.0.2/tests/strategies_builtin/volume_anomaly/test_alpha_features.py +0 -201
  87. deeptrade_quant-0.0.2/tests/strategies_builtin/volume_anomaly/test_candidate_features.py +0 -344
  88. deeptrade_quant-0.0.2/tests/strategies_builtin/volume_anomaly/test_dimension_scores.py +0 -128
  89. deeptrade_quant-0.0.2/tests/strategies_builtin/volume_anomaly/test_prompt_consistency.py +0 -186
  90. deeptrade_quant-0.0.2/tests/strategies_builtin/volume_anomaly/test_realized_returns.py +0 -167
  91. deeptrade_quant-0.0.2/tests/strategies_builtin/volume_anomaly/test_screen_rules.py +0 -225
  92. deeptrade_quant-0.0.2/tests/strategies_builtin/volume_anomaly/test_stats_query.py +0 -167
  93. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/LICENSE +0 -0
  94. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/cli_data.py +0 -0
  95. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/core/__init__.py +0 -0
  96. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/core/llm_client.py +0 -0
  97. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/core/logging_config.py +0 -0
  98. {deeptrade_quant-0.0.2/deeptrade/channels_builtin → deeptrade_quant-0.2.0/deeptrade/core/migrations}/__init__.py +0 -0
  99. {deeptrade_quant-0.0.2/deeptrade/channels_builtin/stdout → deeptrade_quant-0.2.0/deeptrade/core/migrations/core}/__init__.py +0 -0
  100. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/core/paths.py +0 -0
  101. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/core/run_status.py +0 -0
  102. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/core/secrets.py +0 -0
  103. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/core/tushare_client.py +0 -0
  104. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/plugins_api/__init__.py +0 -0
  105. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/plugins_api/base.py +0 -0
  106. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/plugins_api/channel.py +0 -0
  107. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/plugins_api/events.py +0 -0
  108. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/plugins_api/llm.py +0 -0
  109. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/plugins_api/metadata.py +0 -0
  110. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/plugins_api/notify.py +0 -0
  111. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/theme.py +0 -0
  112. {deeptrade_quant-0.0.2/deeptrade/channels_builtin/stdout/stdout_channel → deeptrade_quant-0.2.0/tests}/__init__.py +0 -0
  113. {deeptrade_quant-0.0.2/deeptrade/core/migrations → deeptrade_quant-0.2.0/tests/cli}/__init__.py +0 -0
  114. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/tests/conftest.py +0 -0
  115. {deeptrade_quant-0.0.2/deeptrade/core/migrations → deeptrade_quant-0.2.0/tests}/core/__init__.py +0 -0
  116. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/tests/core/test_llm_client.py +0 -0
  117. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/tests/core/test_notifier.py +0 -0
  118. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/tests/core/test_paths.py +0 -0
  119. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/tests/core/test_plugin_install.py +0 -0
  120. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/tests/core/test_secrets.py +0 -0
  121. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/tests/core/test_tushare_client.py +0 -0
  122. {deeptrade_quant-0.0.2/deeptrade/strategies_builtin → deeptrade_quant-0.2.0/tests/plugins_api}/__init__.py +0 -0
  123. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/tests/plugins_api/test_notify.py +0 -0
  124. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/tests/plugins_api/test_protocol.py +0 -0
  125. {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/tests/test_smoke.py +0 -0
@@ -28,6 +28,9 @@ htmlcov/
28
28
  *.swp
29
29
  *.swo
30
30
 
31
+ # Claude Code workspace
32
+ .claude/
33
+
31
34
  # Environment
32
35
  .env
33
36
  .env.local
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deeptrade-quant
3
- Version: 0.0.2
3
+ Version: 0.2.0
4
4
  Summary: LLM-driven A-share (Shanghai/Shenzhen main board) stock screening CLI
5
5
  Project-URL: Homepage, https://github.com/ty19880929/deeptrade
6
6
  Project-URL: Repository, https://github.com/ty19880929/deeptrade
@@ -14,6 +14,7 @@ Requires-Dist: click>=8.1
14
14
  Requires-Dist: duckdb>=1.0
15
15
  Requires-Dist: keyring>=25.0
16
16
  Requires-Dist: openai>=1.0
17
+ Requires-Dist: packaging>=23.0
17
18
  Requires-Dist: pandas>=2.2
18
19
  Requires-Dist: pydantic>=2.7
19
20
  Requires-Dist: pyyaml>=6.0
@@ -52,10 +53,18 @@ Description-Content-Type: text/markdown
52
53
  ### 安装
53
54
 
54
55
  ```bash
55
- # 推荐
56
+ # 推荐:pipx 隔离环境(命令名仍是 deeptrade)
57
+ pipx install deeptrade-quant
58
+ # 或
59
+ uv tool install deeptrade-quant
60
+ ```
61
+
62
+ > **注**:PyPI 项目名是 `deeptrade-quant`,CLI 命令是 `deeptrade`,Python 包名是 `deeptrade`(`import deeptrade`)。三者不同是 Python 生态常态(同 `pip install scikit-learn` → `import sklearn`)。
63
+
64
+ ```bash
65
+ # 开发模式(克隆本仓库,editable install)
56
66
  uv sync --all-extras
57
67
  uv run pre-commit install
58
-
59
68
  # 兜底(无 uv)
60
69
  python -m venv .venv && source .venv/bin/activate # Windows: .\.venv\Scripts\activate
61
70
  pip install -e ".[dev]"
@@ -72,12 +81,18 @@ deeptrade config test-llm # 对所有 provider 做连通性自
72
81
  deeptrade config show # 表格展示当前配置(密钥脱敏)
73
82
  ```
74
83
 
75
- ### 安装内置插件并运行
84
+ ### 安装官方插件并运行
85
+
86
+ 官方插件维护在 [DeepTradePluginOfficial](https://github.com/ty19880929/DeepTradePluginOfficial),框架通过短名查注册表 → 拉 GitHub release tarball 自动安装。
76
87
 
77
88
  ```bash
78
- # 安装一个 strategy 插件 + 一个 channel 插件
79
- deeptrade plugin install ./deeptrade/strategies_builtin/limit_up_board -y
80
- deeptrade plugin install ./deeptrade/channels_builtin/stdout -y
89
+ # 浏览注册表
90
+ deeptrade plugin search
91
+
92
+ # 按短名安装(注册表 → 最新 release tag)
93
+ deeptrade plugin install limit-up-board
94
+ deeptrade plugin install volume-anomaly
95
+ deeptrade plugin install stdout-channel
81
96
 
82
97
  deeptrade plugin list # 查看已安装
83
98
 
@@ -95,6 +110,8 @@ deeptrade volume-anomaly prune --days 30 # 剔除追踪 ≥30 日的标的
95
110
  deeptrade stdout-channel test
96
111
  ```
97
112
 
113
+ > **第三方插件 / 本地开发**:`deeptrade plugin install ./path/to/my-plugin` 仍可装本地目录;`deeptrade plugin install https://github.com/owner/repo` 装完整 git 仓库(仓库根需含 `deeptrade_plugin.yaml`)。详见 [`docs/plugin-development.md`](docs/plugin-development.md)。
114
+
98
115
  报告产出在 `~/.deeptrade/reports/<run_id>/`。
99
116
 
100
117
  ## 📦 命令矩阵
@@ -19,10 +19,18 @@
19
19
  ### 安装
20
20
 
21
21
  ```bash
22
- # 推荐
22
+ # 推荐:pipx 隔离环境(命令名仍是 deeptrade)
23
+ pipx install deeptrade-quant
24
+ # 或
25
+ uv tool install deeptrade-quant
26
+ ```
27
+
28
+ > **注**:PyPI 项目名是 `deeptrade-quant`,CLI 命令是 `deeptrade`,Python 包名是 `deeptrade`(`import deeptrade`)。三者不同是 Python 生态常态(同 `pip install scikit-learn` → `import sklearn`)。
29
+
30
+ ```bash
31
+ # 开发模式(克隆本仓库,editable install)
23
32
  uv sync --all-extras
24
33
  uv run pre-commit install
25
-
26
34
  # 兜底(无 uv)
27
35
  python -m venv .venv && source .venv/bin/activate # Windows: .\.venv\Scripts\activate
28
36
  pip install -e ".[dev]"
@@ -39,12 +47,18 @@ deeptrade config test-llm # 对所有 provider 做连通性自
39
47
  deeptrade config show # 表格展示当前配置(密钥脱敏)
40
48
  ```
41
49
 
42
- ### 安装内置插件并运行
50
+ ### 安装官方插件并运行
51
+
52
+ 官方插件维护在 [DeepTradePluginOfficial](https://github.com/ty19880929/DeepTradePluginOfficial),框架通过短名查注册表 → 拉 GitHub release tarball 自动安装。
43
53
 
44
54
  ```bash
45
- # 安装一个 strategy 插件 + 一个 channel 插件
46
- deeptrade plugin install ./deeptrade/strategies_builtin/limit_up_board -y
47
- deeptrade plugin install ./deeptrade/channels_builtin/stdout -y
55
+ # 浏览注册表
56
+ deeptrade plugin search
57
+
58
+ # 按短名安装(注册表 → 最新 release tag)
59
+ deeptrade plugin install limit-up-board
60
+ deeptrade plugin install volume-anomaly
61
+ deeptrade plugin install stdout-channel
48
62
 
49
63
  deeptrade plugin list # 查看已安装
50
64
 
@@ -62,6 +76,8 @@ deeptrade volume-anomaly prune --days 30 # 剔除追踪 ≥30 日的标的
62
76
  deeptrade stdout-channel test
63
77
  ```
64
78
 
79
+ > **第三方插件 / 本地开发**:`deeptrade plugin install ./path/to/my-plugin` 仍可装本地目录;`deeptrade plugin install https://github.com/owner/repo` 装完整 git 仓库(仓库根需含 `deeptrade_plugin.yaml`)。详见 [`docs/plugin-development.md`](docs/plugin-development.md)。
80
+
65
81
  报告产出在 `~/.deeptrade/reports/<run_id>/`。
66
82
 
67
83
  ## 📦 命令矩阵
@@ -4,5 +4,5 @@ from __future__ import annotations
4
4
 
5
5
  from deeptrade.core.notifier import notification_session, notify
6
6
 
7
- __version__ = "0.0.1"
7
+ __version__ = "0.2.0"
8
8
  __all__ = ["__version__", "notification_session", "notify"]
@@ -184,14 +184,8 @@ def init(
184
184
  fresh = not db_file.exists()
185
185
  db = Database(db_file)
186
186
  try:
187
- applied = apply_core_migrations(db)
188
187
  if fresh:
189
188
  typer.echo(f"✔ Database created: {db_file}")
190
- if applied:
191
- for v in applied:
192
- typer.echo(f"✔ Schema applied: {v}")
193
- else:
194
- typer.echo("✔ Database already initialized; schema up-to-date")
195
189
  finally:
196
190
  db.close()
197
191
 
@@ -210,5 +204,38 @@ def init(
210
204
  cmd_set_llm()
211
205
 
212
206
 
207
+
208
+ @app.command(name="db", context_settings={"ignore_unknown_options": True, "allow_extra_args": True})
209
+ def db_cmd(ctx: click.Context) -> None:
210
+ """Database migration and management commands (legacy stub; use `deeptrade db init` via group if added)."""
211
+ pass
212
+
213
+ db_app = typer.Typer(name="db", help="Database migration and management commands.")
214
+ app.add_typer(db_app, name="db")
215
+
216
+ @db_app.command("init")
217
+ def db_init() -> None:
218
+ """Initialize the core database tables and apply migrations."""
219
+ paths.ensure_layout()
220
+ db_file = paths.db_path()
221
+ fresh = not db_file.exists()
222
+ db = Database(db_file)
223
+ try:
224
+ applied = apply_core_migrations(db)
225
+ if fresh:
226
+ typer.echo(f"✔ Database created: {db_file}")
227
+ if applied:
228
+ for v in applied:
229
+ typer.echo(f"✔ Schema applied: {v}")
230
+ else:
231
+ typer.echo("✔ Database already initialized; schema up-to-date")
232
+ finally:
233
+ db.close()
234
+
235
+ @db_app.command("upgrade")
236
+ def db_upgrade() -> None:
237
+ """Apply any pending core migrations."""
238
+ db_init()
239
+
213
240
  if __name__ == "__main__":
214
241
  app()
@@ -191,13 +191,35 @@ def _set_llm_new(svc: ConfigService) -> None:
191
191
  typer.echo(f"Provider {name!r} already exists; pick edit instead.")
192
192
  raise typer.Exit(2)
193
193
  default_base = _DEFAULT_BASE_URLS.get(name.split("-")[0], "")
194
- _prompt_and_save_provider(svc, name, defaults=None, default_base_url=default_base)
194
+ # Adding into an empty dict auto-promotes to default; offer the choice
195
+ # only when a default is already in place.
196
+ promote_default: bool | None = None
197
+ if cfg.llm_providers:
198
+ promote_default = bool(
199
+ questionary.confirm(
200
+ f"Set {name!r} as the default LLM provider?",
201
+ default=False,
202
+ ).ask()
203
+ )
204
+ _prompt_and_save_provider(
205
+ svc,
206
+ name,
207
+ defaults=None,
208
+ default_base_url=default_base,
209
+ is_default=promote_default,
210
+ )
195
211
 
196
212
 
197
213
  def _set_llm_edit(svc: ConfigService, name: str) -> None:
198
214
  cfg = svc.get_app_config()
199
215
  cur = cfg.llm_providers[name]
200
- _prompt_and_save_provider(svc, name, defaults=cur.model_dump(), default_base_url=cur.base_url)
216
+ _prompt_and_save_provider(
217
+ svc,
218
+ name,
219
+ defaults=cur.model_dump(),
220
+ default_base_url=cur.base_url,
221
+ is_default=None,
222
+ )
201
223
 
202
224
 
203
225
  def _prompt_and_save_provider(
@@ -206,6 +228,7 @@ def _prompt_and_save_provider(
206
228
  *,
207
229
  defaults: dict | None,
208
230
  default_base_url: str,
231
+ is_default: bool | None = None,
209
232
  ) -> None:
210
233
  base_url_default = defaults.get("base_url", default_base_url) if defaults else default_base_url
211
234
  model_default = defaults.get("model", "") if defaults else ""
@@ -251,6 +274,7 @@ def _prompt_and_save_provider(
251
274
  model=model,
252
275
  timeout=timeout,
253
276
  api_key=api_key if api_key else None,
277
+ is_default=is_default,
254
278
  )
255
279
  except ValueError as e:
256
280
  typer.echo(f"Invalid provider: {e}")
@@ -274,8 +298,48 @@ def _set_llm_delete(svc: ConfigService, existing: list[str]) -> None:
274
298
  ).ask()
275
299
  if not confirm:
276
300
  raise typer.Exit(1)
301
+ prior_default = svc.get_default_llm_provider()
277
302
  svc.delete_llm_provider(name)
278
303
  typer.echo(f"✔ Deleted LLM provider {name!r}")
304
+ new_default = svc.get_default_llm_provider()
305
+ if prior_default == name and new_default is not None:
306
+ typer.echo(f"✔ Default LLM provider auto-switched to {new_default!r}")
307
+
308
+
309
+ @app.command("set-default-llm")
310
+ def cmd_set_default_llm(
311
+ name: str = typer.Argument(..., help="Provider name to mark as default."),
312
+ ) -> None:
313
+ """Mark ``name`` as the default LLM provider.
314
+
315
+ The default is consumed by ``LLMManager.get_client()`` when callers
316
+ don't name a provider (non-debate plugin path). Switching default
317
+ clears the flag on every other provider so the
318
+ "exactly-one-default" invariant holds.
319
+ """
320
+ db, svc = _open_service()
321
+ try:
322
+ cfg = svc.get_app_config()
323
+ provider = cfg.llm_providers.get(name)
324
+ if provider is None:
325
+ typer.echo(
326
+ f"Unknown LLM provider: {name!r}; configured providers: "
327
+ + (", ".join(sorted(cfg.llm_providers.keys())) or "(none)")
328
+ )
329
+ raise typer.Exit(2)
330
+ if provider.is_default:
331
+ typer.echo(f"{name!r} is already the default LLM provider")
332
+ return
333
+ svc.set_llm_provider(
334
+ name,
335
+ base_url=provider.base_url,
336
+ model=provider.model,
337
+ timeout=provider.timeout,
338
+ is_default=True,
339
+ )
340
+ typer.echo(f"✔ Default LLM provider set to {name!r}")
341
+ finally:
342
+ db.close()
279
343
 
280
344
 
281
345
  @app.command("list-llm")
@@ -0,0 +1,330 @@
1
+ """`deeptrade plugin` subcommand group."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import questionary
6
+ import typer
7
+ import yaml
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+
11
+ from deeptrade.core import paths
12
+ from deeptrade.core.db import Database
13
+ from deeptrade.core.github_fetch import (
14
+ GitHubFetchError,
15
+ NoMatchingReleaseError,
16
+ TarballFetchError,
17
+ )
18
+ from deeptrade.core.plugin_manager import (
19
+ PluginInstallError,
20
+ PluginManager,
21
+ PluginNotFoundError,
22
+ UpgradeNoop,
23
+ _load_metadata_yaml,
24
+ summarize_for_install,
25
+ )
26
+ from deeptrade.core.plugin_source import (
27
+ ResolvedSource,
28
+ SourceResolveError,
29
+ SourceResolver,
30
+ )
31
+ from deeptrade.core.registry import (
32
+ RegistryClient,
33
+ RegistryFetchError,
34
+ RegistryNotFoundError,
35
+ RegistrySchemaError,
36
+ )
37
+
38
+ app = typer.Typer(help="Install / manage plugins", no_args_is_help=True)
39
+
40
+ _RESOLVE_ERRORS: tuple[type[Exception], ...] = (
41
+ RegistryNotFoundError,
42
+ RegistryFetchError,
43
+ RegistrySchemaError,
44
+ NoMatchingReleaseError,
45
+ TarballFetchError,
46
+ GitHubFetchError,
47
+ SourceResolveError,
48
+ )
49
+
50
+
51
+ def _open() -> tuple[Database, PluginManager]:
52
+ db = Database(paths.db_path())
53
+ return db, PluginManager(db)
54
+
55
+
56
+ def _format_origin(resolved: ResolvedSource) -> str:
57
+ d = resolved.origin_detail
58
+ if resolved.origin == "local":
59
+ return f"本地路径 ({d.get('local_path', resolved.path)})"
60
+ if resolved.origin == "github_registry":
61
+ return (
62
+ f"GitHub 注册表 ({d['repo']}@{d['ref']}, subdir={d['subdir']})"
63
+ )
64
+ if resolved.origin == "github_url":
65
+ return f"GitHub URL ({d['repo']}@{d['ref']})"
66
+ return resolved.origin
67
+
68
+
69
+ @app.command("install")
70
+ def cmd_install(
71
+ source: str = typer.Argument(
72
+ ..., help="短名(注册表)/ 本地路径 / GitHub URL"
73
+ ),
74
+ ref: str | None = typer.Option(
75
+ None, "--ref", help="Tag / branch / sha (默认 = 该插件最新 release)"
76
+ ),
77
+ yes: bool = typer.Option(False, "-y", "--yes", help="Skip the confirmation prompt"),
78
+ ) -> None:
79
+ """Install a plugin from the registry, a GitHub URL, or a local directory."""
80
+ resolver = SourceResolver()
81
+ try:
82
+ resolved = resolver.resolve(source, ref)
83
+ except _RESOLVE_ERRORS as e:
84
+ typer.echo(f"✘ {e}")
85
+ raise typer.Exit(2) from e
86
+
87
+ try:
88
+ try:
89
+ meta = _load_metadata_yaml(resolved.path / "deeptrade_plugin.yaml")
90
+ except PluginInstallError as e:
91
+ typer.echo(f"✘ {e}")
92
+ raise typer.Exit(2) from e
93
+
94
+ typer.echo("─── 即将安装 ─────────────────────────────")
95
+ typer.echo(f"来源: {_format_origin(resolved)}")
96
+ typer.echo(summarize_for_install(meta, resolved.path))
97
+ typer.echo("──────────────────────────────────────────")
98
+ if not yes:
99
+ ok = questionary.confirm("确认安装?", default=False).ask()
100
+ if not ok:
101
+ typer.echo("Aborted.")
102
+ raise typer.Exit(1)
103
+
104
+ db, mgr = _open()
105
+ try:
106
+ rec = mgr.install(resolved.path)
107
+ except PluginInstallError as e:
108
+ typer.echo(f"✘ Install failed: {e}")
109
+ raise typer.Exit(2) from e
110
+ finally:
111
+ db.close()
112
+
113
+ typer.echo(f"✔ 已安装: {rec.plugin_id} v{rec.version}")
114
+ finally:
115
+ if resolved.cleanup is not None:
116
+ resolved.cleanup()
117
+
118
+
119
+ @app.command("list")
120
+ def cmd_list() -> None:
121
+ """List installed plugins."""
122
+ db, mgr = _open()
123
+ try:
124
+ records = mgr.list_all()
125
+ finally:
126
+ db.close()
127
+
128
+ console = Console()
129
+ table = Table(title="Installed Plugins")
130
+ table.add_column("plugin_id", style="cyan")
131
+ table.add_column("name")
132
+ table.add_column("version")
133
+ table.add_column("enabled", style="green")
134
+ if not records:
135
+ typer.echo("(no plugins installed)")
136
+ return
137
+ for r in records:
138
+ table.add_row(r.plugin_id, r.name, r.version, "yes" if r.enabled else "no")
139
+ console.print(table)
140
+
141
+
142
+ @app.command("info")
143
+ def cmd_info(plugin_id: str = typer.Argument(...)) -> None:
144
+ """Show metadata for a plugin.
145
+
146
+ If installed locally: shows the full installed metadata.yaml.
147
+ If not installed: falls back to the registry entry (with an install hint).
148
+ """
149
+ db, mgr = _open()
150
+ try:
151
+ try:
152
+ rec = mgr.info(plugin_id)
153
+ typer.echo(
154
+ yaml.safe_dump(rec.metadata.model_dump(mode="json"), allow_unicode=True)
155
+ )
156
+ return
157
+ except PluginNotFoundError:
158
+ pass # fall through to registry lookup
159
+ finally:
160
+ db.close()
161
+
162
+ try:
163
+ entry = RegistryClient().resolve(plugin_id)
164
+ except RegistryNotFoundError as e:
165
+ typer.echo(f"✘ {plugin_id} 既未安装,也不在注册表中")
166
+ raise typer.Exit(2) from e
167
+ except (RegistryFetchError, RegistrySchemaError) as e:
168
+ typer.echo(f"✘ {plugin_id} 未安装;查询注册表失败: {e}")
169
+ raise typer.Exit(2) from e
170
+
171
+ typer.echo(f"(未安装) {entry.plugin_id}")
172
+ typer.echo(f" name: {entry.name}")
173
+ typer.echo(f" type: {entry.type}")
174
+ typer.echo(f" description: {entry.description}")
175
+ typer.echo(f" repo: {entry.repo}")
176
+ typer.echo(f" subdir: {entry.subdir}")
177
+ typer.echo(f" install: deeptrade plugin install {entry.plugin_id}")
178
+
179
+
180
+ @app.command("disable")
181
+ def cmd_disable(plugin_id: str = typer.Argument(...)) -> None:
182
+ db, mgr = _open()
183
+ try:
184
+ try:
185
+ mgr.disable(plugin_id)
186
+ except PluginNotFoundError as e:
187
+ typer.echo(f"✘ {plugin_id} not installed")
188
+ raise typer.Exit(2) from e
189
+ typer.echo(f"✔ disabled: {plugin_id}")
190
+ finally:
191
+ db.close()
192
+
193
+
194
+ @app.command("enable")
195
+ def cmd_enable(plugin_id: str = typer.Argument(...)) -> None:
196
+ db, mgr = _open()
197
+ try:
198
+ try:
199
+ mgr.enable(plugin_id)
200
+ except PluginNotFoundError as e:
201
+ typer.echo(f"✘ {plugin_id} not installed")
202
+ raise typer.Exit(2) from e
203
+ typer.echo(f"✔ enabled: {plugin_id}")
204
+ finally:
205
+ db.close()
206
+
207
+
208
+ @app.command("uninstall")
209
+ def cmd_uninstall(
210
+ plugin_id: str = typer.Argument(...),
211
+ purge: bool = typer.Option(False, "--purge", help="DROP plugin tables and forget all data"),
212
+ yes: bool = typer.Option(False, "-y", "--yes", help="Skip confirmation"),
213
+ ) -> None:
214
+ """Uninstall a plugin. Default: keep tables (just disable). --purge: drop tables."""
215
+ db, mgr = _open()
216
+ try:
217
+ try:
218
+ rec = mgr.info(plugin_id)
219
+ except PluginNotFoundError as e:
220
+ typer.echo(f"✘ {plugin_id} not installed")
221
+ raise typer.Exit(2) from e
222
+
223
+ if purge and not yes:
224
+ tables = [t.name for t in rec.metadata.tables if t.purge_on_uninstall]
225
+ typer.echo(f"将删除以下表(不可恢复): {tables}")
226
+ ok = questionary.confirm("确认 --purge?", default=False).ask()
227
+ if not ok:
228
+ typer.echo("Aborted.")
229
+ raise typer.Exit(1)
230
+
231
+ result = mgr.uninstall(plugin_id, purge=purge)
232
+ action = "purged" if purge else "disabled"
233
+ typer.echo(f"✔ {action}: {plugin_id} (dropped tables: {result['purged_tables']})")
234
+ finally:
235
+ db.close()
236
+
237
+
238
+ @app.command("upgrade")
239
+ def cmd_upgrade(
240
+ source: str = typer.Argument(
241
+ ..., help="短名(注册表)/ 本地路径 / GitHub URL"
242
+ ),
243
+ ref: str | None = typer.Option(
244
+ None, "--ref", help="Tag / branch / sha (默认 = 该插件最新 release)"
245
+ ),
246
+ ) -> None:
247
+ """Upgrade an installed plugin.
248
+
249
+ Exit codes:
250
+ 0 — upgraded, or already at the candidate version
251
+ 2 — not installed / candidate < installed (downgrade forbidden) /
252
+ network or registry failure
253
+ """
254
+ resolver = SourceResolver()
255
+ try:
256
+ resolved = resolver.resolve(source, ref)
257
+ except _RESOLVE_ERRORS as e:
258
+ typer.echo(f"✘ {e}")
259
+ raise typer.Exit(2) from e
260
+
261
+ try:
262
+ db, mgr = _open()
263
+ try:
264
+ try:
265
+ result = mgr.upgrade(resolved.path)
266
+ except PluginNotFoundError as e:
267
+ try:
268
+ meta = _load_metadata_yaml(resolved.path / "deeptrade_plugin.yaml")
269
+ pid = meta.plugin_id
270
+ except PluginInstallError:
271
+ pid = source
272
+ typer.echo(
273
+ f'✘ 插件 "{pid}" 未安装,请先执行 deeptrade plugin install'
274
+ )
275
+ raise typer.Exit(2) from e
276
+ except PluginInstallError as e:
277
+ typer.echo(f"✘ Upgrade failed: {e}")
278
+ raise typer.Exit(2) from e
279
+
280
+ if isinstance(result, UpgradeNoop):
281
+ typer.echo(f"已是最新版本 v{result.version}")
282
+ return
283
+ typer.echo(f"✔ upgraded: {result.plugin_id} → v{result.version}")
284
+ finally:
285
+ db.close()
286
+ finally:
287
+ if resolved.cleanup is not None:
288
+ resolved.cleanup()
289
+
290
+
291
+ @app.command("search")
292
+ def cmd_search(
293
+ keyword: str | None = typer.Argument(
294
+ None, help="可选过滤关键词(匹配 plugin_id / name / description)"
295
+ ),
296
+ no_cache: bool = typer.Option(
297
+ False, "--no-cache", help="强制刷新注册表(旁路 ETag 缓存)"
298
+ ),
299
+ ) -> None:
300
+ """List plugins available in the official registry."""
301
+ try:
302
+ registry = RegistryClient().fetch(force=no_cache)
303
+ except (RegistryFetchError, RegistrySchemaError) as e:
304
+ typer.echo(f"✘ {e}")
305
+ raise typer.Exit(2) from e
306
+
307
+ rows = list(registry.plugins.values())
308
+ if keyword:
309
+ kw = keyword.lower()
310
+ rows = [
311
+ entry
312
+ for entry in rows
313
+ if kw in entry.plugin_id.lower()
314
+ or kw in entry.name.lower()
315
+ or kw in entry.description.lower()
316
+ ]
317
+
318
+ if not rows:
319
+ typer.echo("(no plugins matched)" if keyword else "(registry is empty)")
320
+ return
321
+
322
+ console = Console()
323
+ table = Table(title="Available Plugins")
324
+ table.add_column("plugin_id", style="cyan")
325
+ table.add_column("name")
326
+ table.add_column("type", style="green")
327
+ table.add_column("description")
328
+ for entry in sorted(rows, key=lambda x: x.plugin_id):
329
+ table.add_row(entry.plugin_id, entry.name, entry.type, entry.description)
330
+ console.print(table)