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/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
plugin_id: stdout-channel
|
|
2
|
+
name: Stdout Channel
|
|
3
|
+
version: 0.1.0
|
|
4
|
+
type: channel
|
|
5
|
+
api_version: "1"
|
|
6
|
+
entrypoint: stdout_channel.channel:StdoutChannel
|
|
7
|
+
description: Reference notification channel — fully consumes the payload but only prints "✔ push success" to stdout.
|
|
8
|
+
author: DeepTrade
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
tushare_apis:
|
|
12
|
+
required: []
|
|
13
|
+
optional: []
|
|
14
|
+
llm: false
|
|
15
|
+
llm_tools: false
|
|
16
|
+
|
|
17
|
+
migrations:
|
|
18
|
+
- version: "20260429_001"
|
|
19
|
+
file: migrations/20260429_001_init.sql
|
|
20
|
+
checksum: "sha256:7317d23d4ac91b9a1259e2ba4d990863491de8fe1115e19052b02f83939399cc"
|
|
21
|
+
|
|
22
|
+
tables:
|
|
23
|
+
- name: stdout_channel_log
|
|
24
|
+
description: Audit log of payloads delivered through the stdout channel
|
|
25
|
+
purge_on_uninstall: true
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
-- stdout channel: delivery log so we can verify the payload was actually
|
|
2
|
+
-- consumed (not just acknowledged with a fake "✔ push success").
|
|
3
|
+
CREATE TABLE IF NOT EXISTS stdout_channel_log (
|
|
4
|
+
delivered_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
5
|
+
plugin_id VARCHAR NOT NULL,
|
|
6
|
+
run_id UUID NOT NULL,
|
|
7
|
+
status VARCHAR NOT NULL,
|
|
8
|
+
title VARCHAR NOT NULL,
|
|
9
|
+
section_count INTEGER NOT NULL,
|
|
10
|
+
item_count INTEGER NOT NULL,
|
|
11
|
+
metric_count INTEGER NOT NULL,
|
|
12
|
+
PRIMARY KEY (run_id, plugin_id, delivered_at)
|
|
13
|
+
);
|
|
File without changes
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Stdout reference channel plugin.
|
|
2
|
+
|
|
3
|
+
Implements the framework's ``ChannelPlugin`` Protocol:
|
|
4
|
+
* ``validate_static`` — install-time self-check (no network)
|
|
5
|
+
* ``dispatch(argv)`` — CLI dispatch (subcommands: test, log)
|
|
6
|
+
* ``push(ctx, payload)`` — receive a NotificationPayload from the notifier
|
|
7
|
+
|
|
8
|
+
Behaviour on push:
|
|
9
|
+
* FULLY consume the NotificationPayload (walk every section/item/metric)
|
|
10
|
+
so any structural bug surfaces immediately.
|
|
11
|
+
* Persist a one-row audit record to ``stdout_channel_log``.
|
|
12
|
+
* Emit ONE concise line: "✔ push success (...)".
|
|
13
|
+
|
|
14
|
+
This channel is the zero-dependency target for unit tests of the notify
|
|
15
|
+
plumbing and is safe to ship enabled-by-default in dev installs.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import sys
|
|
22
|
+
from typing import TYPE_CHECKING
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
25
|
+
from deeptrade.plugins_api.base import PluginContext
|
|
26
|
+
from deeptrade.plugins_api.notify import NotificationPayload
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class StdoutChannel:
|
|
30
|
+
"""Stdout-only IM channel plugin."""
|
|
31
|
+
|
|
32
|
+
metadata = None # injected by framework after install
|
|
33
|
+
|
|
34
|
+
# ----- Plugin Protocol -------------------------------------------------
|
|
35
|
+
|
|
36
|
+
def validate_static(self, ctx: PluginContext) -> None: # noqa: ARG002
|
|
37
|
+
# Sanity: the audit table must exist (created by our own migration).
|
|
38
|
+
# If it's missing, the install pipeline already failed; this is just a
|
|
39
|
+
# belt-and-braces self-check that doesn't touch the network.
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
def dispatch(self, argv: list[str]) -> int:
|
|
43
|
+
"""Channel-side CLI: ``test`` and ``log``.
|
|
44
|
+
|
|
45
|
+
``test`` synthesizes a minimal payload and routes it back through this
|
|
46
|
+
channel's own ``push`` so the user can verify the channel is wired up
|
|
47
|
+
without running a real strategy. ``log`` dumps the most recent
|
|
48
|
+
``stdout_channel_log`` rows.
|
|
49
|
+
"""
|
|
50
|
+
parser = argparse.ArgumentParser(
|
|
51
|
+
prog="deeptrade stdout-channel",
|
|
52
|
+
description="Stdout notification channel — local self-test + audit log.",
|
|
53
|
+
)
|
|
54
|
+
sub = parser.add_subparsers(dest="cmd", required=False)
|
|
55
|
+
|
|
56
|
+
sub.add_parser("test", help="Push a synthetic payload through this channel.")
|
|
57
|
+
log_p = sub.add_parser("log", help="Print recent delivery audit rows.")
|
|
58
|
+
log_p.add_argument("--limit", type=int, default=20)
|
|
59
|
+
|
|
60
|
+
args = parser.parse_args(argv)
|
|
61
|
+
if args.cmd is None:
|
|
62
|
+
parser.print_help()
|
|
63
|
+
return 0
|
|
64
|
+
if args.cmd == "test":
|
|
65
|
+
return self._cmd_test()
|
|
66
|
+
if args.cmd == "log":
|
|
67
|
+
return self._cmd_log(args.limit)
|
|
68
|
+
return 2
|
|
69
|
+
|
|
70
|
+
# ----- ChannelPlugin Protocol ------------------------------------------
|
|
71
|
+
|
|
72
|
+
def push(self, ctx: PluginContext, payload: NotificationPayload) -> None:
|
|
73
|
+
# 1) Walk the payload — counts come from real iteration, never from
|
|
74
|
+
# dict.len(), so we cannot "succeed" without having actually read the
|
|
75
|
+
# plugin's data.
|
|
76
|
+
section_count = len(payload.sections)
|
|
77
|
+
item_count = sum(len(s.items) for s in payload.sections)
|
|
78
|
+
metric_count = len(payload.metrics)
|
|
79
|
+
for section in payload.sections:
|
|
80
|
+
for item in section.items:
|
|
81
|
+
_ = (item.code, item.name, item.rank, item.score, item.label, item.note)
|
|
82
|
+
for k, v in payload.metrics.items():
|
|
83
|
+
_ = (k, v)
|
|
84
|
+
_ = payload.title, payload.summary, payload.report_dir, payload.extras
|
|
85
|
+
|
|
86
|
+
# 2) Persist a delivery audit row.
|
|
87
|
+
ctx.db.execute(
|
|
88
|
+
"INSERT INTO stdout_channel_log(plugin_id, run_id, status, title, "
|
|
89
|
+
"section_count, item_count, metric_count) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
90
|
+
(
|
|
91
|
+
payload.plugin_id,
|
|
92
|
+
payload.run_id,
|
|
93
|
+
payload.status.value,
|
|
94
|
+
payload.title,
|
|
95
|
+
section_count,
|
|
96
|
+
item_count,
|
|
97
|
+
metric_count,
|
|
98
|
+
),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# 3) One concise line. Use sys.stdout (not rich console) so this stays
|
|
102
|
+
# out of any TUI / strategy console that the caller may have running.
|
|
103
|
+
sys.stdout.write(
|
|
104
|
+
f"✔ push success (channel=stdout-channel run_id={payload.run_id} "
|
|
105
|
+
f"status={payload.status.value})\n"
|
|
106
|
+
)
|
|
107
|
+
sys.stdout.flush()
|
|
108
|
+
|
|
109
|
+
# ----- private helpers -------------------------------------------------
|
|
110
|
+
|
|
111
|
+
def _cmd_test(self) -> int:
|
|
112
|
+
"""Round-trip a synthetic payload through ``deeptrade.notify``.
|
|
113
|
+
|
|
114
|
+
Goes through the framework's notifier layer (not directly through
|
|
115
|
+
``self.push``) so the test exercises the full path: install →
|
|
116
|
+
build_notifier → dispatch → push.
|
|
117
|
+
"""
|
|
118
|
+
import uuid
|
|
119
|
+
|
|
120
|
+
from deeptrade import notify
|
|
121
|
+
from deeptrade.core import paths
|
|
122
|
+
from deeptrade.core.db import Database
|
|
123
|
+
from deeptrade.core.run_status import RunStatus
|
|
124
|
+
from deeptrade.plugins_api.notify import (
|
|
125
|
+
NotificationItem,
|
|
126
|
+
NotificationPayload,
|
|
127
|
+
NotificationSection,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
payload = NotificationPayload(
|
|
131
|
+
plugin_id="stdout-channel",
|
|
132
|
+
run_id=str(uuid.uuid4()),
|
|
133
|
+
status=RunStatus.SUCCESS,
|
|
134
|
+
title="DeepTrade — stdout channel test",
|
|
135
|
+
summary="Synthetic payload from `deeptrade stdout-channel test`.",
|
|
136
|
+
sections=[
|
|
137
|
+
NotificationSection(
|
|
138
|
+
key="demo",
|
|
139
|
+
title="Demo items",
|
|
140
|
+
items=[
|
|
141
|
+
NotificationItem(
|
|
142
|
+
code="600519.SH", name="贵州茅台", rank=1, score=87.5,
|
|
143
|
+
label="top_candidate", note="演示条目,非真实推荐",
|
|
144
|
+
),
|
|
145
|
+
],
|
|
146
|
+
),
|
|
147
|
+
],
|
|
148
|
+
metrics={"selected": 1},
|
|
149
|
+
)
|
|
150
|
+
db = Database(paths.db_path())
|
|
151
|
+
try:
|
|
152
|
+
notify(db, payload)
|
|
153
|
+
finally:
|
|
154
|
+
db.close()
|
|
155
|
+
return 0
|
|
156
|
+
|
|
157
|
+
def _cmd_log(self, limit: int) -> int:
|
|
158
|
+
from deeptrade.core import paths
|
|
159
|
+
from deeptrade.core.db import Database
|
|
160
|
+
|
|
161
|
+
db = Database(paths.db_path())
|
|
162
|
+
try:
|
|
163
|
+
rows = db.fetchall(
|
|
164
|
+
"SELECT delivered_at, plugin_id, run_id, status, title, "
|
|
165
|
+
"section_count, item_count, metric_count "
|
|
166
|
+
"FROM stdout_channel_log ORDER BY delivered_at DESC LIMIT ?",
|
|
167
|
+
(limit,),
|
|
168
|
+
)
|
|
169
|
+
finally:
|
|
170
|
+
db.close()
|
|
171
|
+
|
|
172
|
+
if not rows:
|
|
173
|
+
sys.stdout.write("(no deliveries yet)\n")
|
|
174
|
+
return 0
|
|
175
|
+
for r in rows:
|
|
176
|
+
sys.stdout.write(
|
|
177
|
+
f"{r[0]} {r[1]:<20} {r[3]:<8} sec={r[5]} item={r[6]} "
|
|
178
|
+
f"metric={r[7]} {r[4]}\n"
|
|
179
|
+
)
|
|
180
|
+
return 0
|
deeptrade/cli.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""DeepTrade CLI entry point.
|
|
2
|
+
|
|
3
|
+
Framework command surface (closed):
|
|
4
|
+
|
|
5
|
+
deeptrade --version | -V
|
|
6
|
+
deeptrade --help | -h
|
|
7
|
+
deeptrade init [--no-prompts]
|
|
8
|
+
deeptrade config {show, set, set-tushare, set-llm, list-llm, test-llm}
|
|
9
|
+
deeptrade plugin {install, list, info, enable, disable, uninstall, upgrade}
|
|
10
|
+
deeptrade data sync ... (stub — pending refactor)
|
|
11
|
+
|
|
12
|
+
Plugin commands are dispatched through pure pass-through:
|
|
13
|
+
|
|
14
|
+
deeptrade <plugin_id> <argv...> → plugin.dispatch(argv)
|
|
15
|
+
|
|
16
|
+
The framework knows nothing about a plugin's subcommand tree. ``--help`` for a
|
|
17
|
+
plugin is the plugin's own responsibility.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import sys
|
|
23
|
+
|
|
24
|
+
import click
|
|
25
|
+
import typer
|
|
26
|
+
from typer.core import TyperGroup
|
|
27
|
+
|
|
28
|
+
from deeptrade import __version__
|
|
29
|
+
from deeptrade.cli_config import app as config_app
|
|
30
|
+
from deeptrade.cli_data import app as data_app
|
|
31
|
+
from deeptrade.cli_plugin import app as plugin_app
|
|
32
|
+
from deeptrade.core import paths
|
|
33
|
+
from deeptrade.core.db import Database, apply_core_migrations
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Custom click.Group implementing pure plugin pass-through
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class _DeepTradeGroup(TyperGroup):
|
|
41
|
+
"""Top-level group that falls back to plugin dispatch for unknown commands.
|
|
42
|
+
|
|
43
|
+
Resolution order on ``deeptrade <token> ...``:
|
|
44
|
+
|
|
45
|
+
1. If ``<token>`` is a registered framework command → click handles it.
|
|
46
|
+
2. Otherwise look it up in the ``plugins`` table:
|
|
47
|
+
- found + enabled → load entrypoint, call ``plugin.dispatch(argv)``
|
|
48
|
+
- found + disabled → exit 2 with "enable first" hint
|
|
49
|
+
- not found → exit 2 with "unknown" + framework cmd list
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
# Disable click's "no such command" so we can route to plugins instead.
|
|
53
|
+
def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None:
|
|
54
|
+
registered = super().get_command(ctx, cmd_name)
|
|
55
|
+
if registered is not None:
|
|
56
|
+
return registered
|
|
57
|
+
return _build_plugin_command(cmd_name)
|
|
58
|
+
|
|
59
|
+
def resolve_command(
|
|
60
|
+
self, ctx: click.Context, args: list[str]
|
|
61
|
+
) -> tuple[str | None, click.Command | None, list[str]]:
|
|
62
|
+
# Standard resolution: returns (cmd_name, cmd, remaining_args).
|
|
63
|
+
cmd_name = args[0] if args else None
|
|
64
|
+
cmd = self.get_command(ctx, cmd_name) if cmd_name else None
|
|
65
|
+
if cmd is None and cmd_name is not None:
|
|
66
|
+
# Synthesize a helpful error including framework cmd list.
|
|
67
|
+
framework = sorted(super().list_commands(ctx))
|
|
68
|
+
ctx.fail(
|
|
69
|
+
f"unknown command or plugin: {cmd_name!r}\n"
|
|
70
|
+
f" framework commands: {framework}\n"
|
|
71
|
+
f" use `deeptrade plugin list` to see installed plugins"
|
|
72
|
+
)
|
|
73
|
+
return super().resolve_command(ctx, args)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _build_plugin_command(plugin_id: str) -> click.Command | None:
|
|
77
|
+
"""Resolve ``plugin_id`` against the installed plugins; return a click
|
|
78
|
+
Command that dispatches to ``plugin.dispatch(remaining_argv)``."""
|
|
79
|
+
from pathlib import Path
|
|
80
|
+
|
|
81
|
+
from deeptrade.core.plugin_manager import (
|
|
82
|
+
PluginManager,
|
|
83
|
+
PluginNotFoundError,
|
|
84
|
+
_load_entrypoint,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
db = Database(paths.db_path())
|
|
88
|
+
try:
|
|
89
|
+
mgr = PluginManager(db)
|
|
90
|
+
try:
|
|
91
|
+
rec = mgr.info(plugin_id)
|
|
92
|
+
except PluginNotFoundError:
|
|
93
|
+
return None
|
|
94
|
+
finally:
|
|
95
|
+
db.close()
|
|
96
|
+
|
|
97
|
+
if not rec.enabled:
|
|
98
|
+
|
|
99
|
+
@click.command(
|
|
100
|
+
name=plugin_id,
|
|
101
|
+
help=f"(disabled) {rec.name}",
|
|
102
|
+
context_settings={"ignore_unknown_options": True, "allow_extra_args": True},
|
|
103
|
+
)
|
|
104
|
+
def _disabled() -> None:
|
|
105
|
+
typer.echo(
|
|
106
|
+
f"✘ plugin {plugin_id!r} is disabled; "
|
|
107
|
+
f"run `deeptrade plugin enable {plugin_id}`"
|
|
108
|
+
)
|
|
109
|
+
raise typer.Exit(2)
|
|
110
|
+
|
|
111
|
+
return _disabled
|
|
112
|
+
|
|
113
|
+
# Enabled: hand the remaining argv straight to the plugin.
|
|
114
|
+
@click.command(
|
|
115
|
+
name=plugin_id,
|
|
116
|
+
# Plugin owns its own --help; let everything through unparsed.
|
|
117
|
+
help=f"{rec.name} (v{rec.version}) — plugin-managed CLI; try `--help`",
|
|
118
|
+
context_settings={
|
|
119
|
+
"ignore_unknown_options": True,
|
|
120
|
+
"allow_extra_args": True,
|
|
121
|
+
"help_option_names": [], # don't intercept --help
|
|
122
|
+
},
|
|
123
|
+
)
|
|
124
|
+
@click.pass_context
|
|
125
|
+
def _dispatch(ctx: click.Context) -> None:
|
|
126
|
+
plugin = _load_entrypoint(Path(rec.install_path), rec.entrypoint, rec.metadata)
|
|
127
|
+
if not hasattr(plugin, "dispatch"):
|
|
128
|
+
typer.echo(f"✘ plugin {plugin_id!r} does not implement dispatch()")
|
|
129
|
+
raise typer.Exit(2)
|
|
130
|
+
rc = plugin.dispatch(list(ctx.args))
|
|
131
|
+
raise typer.Exit(rc or 0)
|
|
132
|
+
|
|
133
|
+
return _dispatch
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
# Typer application (framework commands only)
|
|
138
|
+
# ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
app = typer.Typer(
|
|
142
|
+
name="deeptrade",
|
|
143
|
+
help="DeepTrade — LLM-driven A-share stock screening CLI",
|
|
144
|
+
no_args_is_help=True,
|
|
145
|
+
add_completion=True,
|
|
146
|
+
cls=_DeepTradeGroup,
|
|
147
|
+
)
|
|
148
|
+
app.add_typer(config_app, name="config")
|
|
149
|
+
app.add_typer(plugin_app, name="plugin")
|
|
150
|
+
app.add_typer(data_app, name="data")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _version_callback(value: bool) -> None:
|
|
154
|
+
if value:
|
|
155
|
+
typer.echo(f"DeepTrade {__version__}")
|
|
156
|
+
raise typer.Exit()
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@app.callback()
|
|
160
|
+
def main(
|
|
161
|
+
version: bool = typer.Option( # noqa: ARG001 — consumed by Typer via callback
|
|
162
|
+
False,
|
|
163
|
+
"--version",
|
|
164
|
+
"-V",
|
|
165
|
+
callback=_version_callback,
|
|
166
|
+
is_eager=True,
|
|
167
|
+
help="Show version and exit.",
|
|
168
|
+
),
|
|
169
|
+
) -> None:
|
|
170
|
+
"""DeepTrade — LLM-driven A-share stock screening CLI."""
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@app.command()
|
|
174
|
+
def init(
|
|
175
|
+
no_prompts: bool = typer.Option(
|
|
176
|
+
False,
|
|
177
|
+
"--no-prompts",
|
|
178
|
+
help="Skip post-init tushare/deepseek configuration prompts.",
|
|
179
|
+
),
|
|
180
|
+
) -> None:
|
|
181
|
+
"""Initialize ~/.deeptrade layout and apply core schema migrations (idempotent)."""
|
|
182
|
+
paths.ensure_layout()
|
|
183
|
+
db_file = paths.db_path()
|
|
184
|
+
fresh = not db_file.exists()
|
|
185
|
+
db = Database(db_file)
|
|
186
|
+
try:
|
|
187
|
+
applied = apply_core_migrations(db)
|
|
188
|
+
if fresh:
|
|
189
|
+
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
|
+
finally:
|
|
196
|
+
db.close()
|
|
197
|
+
|
|
198
|
+
if no_prompts or not sys.stdin.isatty():
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
import questionary
|
|
202
|
+
|
|
203
|
+
if questionary.confirm("Configure tushare now?", default=True).ask():
|
|
204
|
+
from deeptrade.cli_config import cmd_set_tushare
|
|
205
|
+
|
|
206
|
+
cmd_set_tushare()
|
|
207
|
+
if questionary.confirm("Configure an LLM provider now?", default=True).ask():
|
|
208
|
+
from deeptrade.cli_config import cmd_set_llm
|
|
209
|
+
|
|
210
|
+
cmd_set_llm()
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
if __name__ == "__main__":
|
|
214
|
+
app()
|