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.
- deeptrade/__init__.py +8 -0
- deeptrade/channels_builtin/__init__.py +0 -0
- deeptrade/channels_builtin/stdout/__init__.py +0 -0
- deeptrade/channels_builtin/stdout/deeptrade_plugin.yaml +25 -0
- deeptrade/channels_builtin/stdout/migrations/20260429_001_init.sql +13 -0
- deeptrade/channels_builtin/stdout/stdout_channel/__init__.py +0 -0
- deeptrade/channels_builtin/stdout/stdout_channel/channel.py +180 -0
- deeptrade/cli.py +214 -0
- deeptrade/cli_config.py +396 -0
- deeptrade/cli_data.py +33 -0
- deeptrade/cli_plugin.py +176 -0
- deeptrade/core/__init__.py +8 -0
- deeptrade/core/config.py +344 -0
- deeptrade/core/config_migrations.py +138 -0
- deeptrade/core/db.py +176 -0
- deeptrade/core/llm_client.py +591 -0
- deeptrade/core/llm_manager.py +174 -0
- deeptrade/core/logging_config.py +61 -0
- deeptrade/core/migrations/__init__.py +0 -0
- deeptrade/core/migrations/core/20260427_001_init.sql +121 -0
- deeptrade/core/migrations/core/20260501_002_drop_llm_calls_stage.sql +10 -0
- deeptrade/core/migrations/core/__init__.py +0 -0
- deeptrade/core/notifier.py +302 -0
- deeptrade/core/paths.py +49 -0
- deeptrade/core/plugin_manager.py +616 -0
- deeptrade/core/run_status.py +29 -0
- deeptrade/core/secrets.py +152 -0
- deeptrade/core/tushare_client.py +824 -0
- deeptrade/plugins_api/__init__.py +44 -0
- deeptrade/plugins_api/base.py +66 -0
- deeptrade/plugins_api/channel.py +42 -0
- deeptrade/plugins_api/events.py +61 -0
- deeptrade/plugins_api/llm.py +46 -0
- deeptrade/plugins_api/metadata.py +84 -0
- deeptrade/plugins_api/notify.py +67 -0
- deeptrade/strategies_builtin/__init__.py +0 -0
- deeptrade/strategies_builtin/limit_up_board/__init__.py +0 -0
- deeptrade/strategies_builtin/limit_up_board/deeptrade_plugin.yaml +101 -0
- deeptrade/strategies_builtin/limit_up_board/limit_up_board/__init__.py +0 -0
- deeptrade/strategies_builtin/limit_up_board/limit_up_board/calendar.py +65 -0
- deeptrade/strategies_builtin/limit_up_board/limit_up_board/cli.py +269 -0
- deeptrade/strategies_builtin/limit_up_board/limit_up_board/config.py +76 -0
- deeptrade/strategies_builtin/limit_up_board/limit_up_board/data.py +1191 -0
- deeptrade/strategies_builtin/limit_up_board/limit_up_board/pipeline.py +869 -0
- deeptrade/strategies_builtin/limit_up_board/limit_up_board/plugin.py +30 -0
- deeptrade/strategies_builtin/limit_up_board/limit_up_board/profiles.py +85 -0
- deeptrade/strategies_builtin/limit_up_board/limit_up_board/prompts.py +485 -0
- deeptrade/strategies_builtin/limit_up_board/limit_up_board/render.py +890 -0
- deeptrade/strategies_builtin/limit_up_board/limit_up_board/runner.py +1087 -0
- deeptrade/strategies_builtin/limit_up_board/limit_up_board/runtime.py +172 -0
- deeptrade/strategies_builtin/limit_up_board/limit_up_board/schemas.py +178 -0
- deeptrade/strategies_builtin/limit_up_board/migrations/20260430_001_init.sql +150 -0
- deeptrade/strategies_builtin/limit_up_board/migrations/20260501_002_lub_stage_results_llm_provider.sql +8 -0
- deeptrade/strategies_builtin/limit_up_board/migrations/20260508_001_lub_lhb_tables.sql +36 -0
- deeptrade/strategies_builtin/limit_up_board/migrations/20260508_002_lub_cyq_perf.sql +18 -0
- deeptrade/strategies_builtin/limit_up_board/migrations/20260508_003_lub_lhb_pk_fix.sql +46 -0
- deeptrade/strategies_builtin/limit_up_board/migrations/20260508_004_lub_lhb_drop_pk.sql +53 -0
- deeptrade/strategies_builtin/limit_up_board/migrations/20260508_005_lub_config.sql +17 -0
- deeptrade/strategies_builtin/volume_anomaly/__init__.py +0 -0
- deeptrade/strategies_builtin/volume_anomaly/deeptrade_plugin.yaml +59 -0
- deeptrade/strategies_builtin/volume_anomaly/migrations/20260430_001_init.sql +94 -0
- deeptrade/strategies_builtin/volume_anomaly/migrations/20260601_001_realized_returns.sql +44 -0
- deeptrade/strategies_builtin/volume_anomaly/migrations/20260601_002_dimension_scores.sql +13 -0
- deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/__init__.py +0 -0
- deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/calendar.py +52 -0
- deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/cli.py +247 -0
- deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/data.py +2154 -0
- deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/pipeline.py +327 -0
- deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/plugin.py +22 -0
- deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/profiles.py +49 -0
- deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/prompts.py +187 -0
- deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/prompts_examples.py +84 -0
- deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/render.py +906 -0
- deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/runner.py +772 -0
- deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/runtime.py +90 -0
- deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/schemas.py +97 -0
- deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/stats.py +174 -0
- deeptrade/theme.py +48 -0
- deeptrade_quant-0.0.2.dist-info/METADATA +166 -0
- deeptrade_quant-0.0.2.dist-info/RECORD +83 -0
- deeptrade_quant-0.0.2.dist-info/WHEEL +4 -0
- deeptrade_quant-0.0.2.dist-info/entry_points.txt +2 -0
- deeptrade_quant-0.0.2.dist-info/licenses/LICENSE +21 -0
deeptrade/cli_config.py
ADDED
|
@@ -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)
|
deeptrade/cli_plugin.py
ADDED
|
@@ -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
|