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
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Public plugin API.
|
|
2
|
+
|
|
3
|
+
All plugins import from this package; the rest of ``deeptrade.*`` is internal.
|
|
4
|
+
|
|
5
|
+
Stable surface (api_version = "1"):
|
|
6
|
+
- Plugin (Protocol), PluginContext — every plugin's contract: metadata + validate_static + dispatch
|
|
7
|
+
- ChannelPlugin (Protocol) — channel-specific extension (push hook)
|
|
8
|
+
- NotificationPayload, NotificationSection, NotificationItem — notify data contract
|
|
9
|
+
- PluginMetadata, TableSpec, MigrationSpec, PluginPermissions, ...
|
|
10
|
+
- StageProfile — LLM 调参档;插件持有 preset → stage 映射表,自行解析
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from deeptrade.plugins_api.base import Plugin, PluginContext
|
|
16
|
+
from deeptrade.plugins_api.channel import ChannelPlugin
|
|
17
|
+
from deeptrade.plugins_api.llm import StageProfile
|
|
18
|
+
from deeptrade.plugins_api.metadata import (
|
|
19
|
+
MigrationSpec,
|
|
20
|
+
PluginMetadata,
|
|
21
|
+
PluginPermissions,
|
|
22
|
+
TableSpec,
|
|
23
|
+
TushareApiPermissions,
|
|
24
|
+
)
|
|
25
|
+
from deeptrade.plugins_api.notify import (
|
|
26
|
+
NotificationItem,
|
|
27
|
+
NotificationPayload,
|
|
28
|
+
NotificationSection,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"ChannelPlugin",
|
|
33
|
+
"MigrationSpec",
|
|
34
|
+
"NotificationItem",
|
|
35
|
+
"NotificationPayload",
|
|
36
|
+
"NotificationSection",
|
|
37
|
+
"Plugin",
|
|
38
|
+
"PluginContext",
|
|
39
|
+
"PluginMetadata",
|
|
40
|
+
"PluginPermissions",
|
|
41
|
+
"StageProfile",
|
|
42
|
+
"TableSpec",
|
|
43
|
+
"TushareApiPermissions",
|
|
44
|
+
]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Plugin contract — api_version "1".
|
|
2
|
+
|
|
3
|
+
A plugin is a Python class implementing this Protocol. The framework loads it
|
|
4
|
+
via the dotted entrypoint declared in the YAML metadata. The framework knows
|
|
5
|
+
NOTHING about the plugin's domain semantics; the plugin owns its own command
|
|
6
|
+
parsing, execution, persistence, and output.
|
|
7
|
+
|
|
8
|
+
``PluginContext`` is the minimal services bundle the framework hands to every
|
|
9
|
+
plugin's ``validate_static`` (during install) and to channel plugins' ``push``
|
|
10
|
+
(during notify). It lives here — alongside the base ``Plugin`` Protocol —
|
|
11
|
+
because it is the universal context for the base contract; channel-specific
|
|
12
|
+
extensions live in :mod:`deeptrade.plugins_api.channel`.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
21
|
+
from deeptrade.core.config import ConfigService
|
|
22
|
+
from deeptrade.core.db import Database
|
|
23
|
+
from deeptrade.plugins_api.metadata import PluginMetadata
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class PluginContext:
|
|
28
|
+
"""Minimal services bundle the framework hands to a plugin's
|
|
29
|
+
``validate_static`` (during install) and to a channel plugin's ``push``
|
|
30
|
+
(during notify).
|
|
31
|
+
|
|
32
|
+
Plugins that need richer services (TushareClient, LLMManager / LLMClient, etc.)
|
|
33
|
+
construct them inside their own ``dispatch`` from these primitives.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
db: Database
|
|
37
|
+
config: ConfigService
|
|
38
|
+
plugin_id: str | None = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@runtime_checkable
|
|
42
|
+
class Plugin(Protocol):
|
|
43
|
+
"""The minimal interface a plugin entrypoint class must satisfy."""
|
|
44
|
+
|
|
45
|
+
metadata: PluginMetadata
|
|
46
|
+
|
|
47
|
+
def validate_static(self, ctx: PluginContext) -> None:
|
|
48
|
+
"""Post-install self-check. MUST NOT touch the network.
|
|
49
|
+
|
|
50
|
+
Called once by the framework during ``deeptrade plugin install``. Use
|
|
51
|
+
it to verify required tables exist, required config keys are present,
|
|
52
|
+
and the entrypoint class loads cleanly. Raise on failure to abort
|
|
53
|
+
install (the framework will roll back).
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def dispatch(self, argv: list[str]) -> int:
|
|
57
|
+
"""CLI dispatch entry point.
|
|
58
|
+
|
|
59
|
+
``argv`` is the remaining command-line tail after the framework strips
|
|
60
|
+
the leading ``<plugin_id>`` token. For example, when the user runs
|
|
61
|
+
``deeptrade limit-up-board run --force-sync``, the plugin receives
|
|
62
|
+
``argv == ['run', '--force-sync']``.
|
|
63
|
+
|
|
64
|
+
The plugin owns parsing, ``--help`` rendering, execution, persistence,
|
|
65
|
+
and output. Returns the process exit code (0 = success).
|
|
66
|
+
"""
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""ChannelPlugin contract — api_version "1".
|
|
2
|
+
|
|
3
|
+
A notification channel is a Python class implementing this Protocol. Channels
|
|
4
|
+
are loaded by the framework via the dotted entrypoint declared in their YAML
|
|
5
|
+
metadata, just like any other plugin.
|
|
6
|
+
|
|
7
|
+
Channel plugins are EXPLICITLY EXEMPTED from the "no direct external API" rule
|
|
8
|
+
that applies to other plugins: a channel IS an HTTP client by nature, so it may
|
|
9
|
+
``import httpx``/``requests`` directly. They still go through ``PluginContext``
|
|
10
|
+
(defined in :mod:`deeptrade.plugins_api.base`) for DB and config access.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
16
|
+
|
|
17
|
+
from deeptrade.plugins_api.base import Plugin, PluginContext
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
20
|
+
from deeptrade.plugins_api.metadata import PluginMetadata
|
|
21
|
+
from deeptrade.plugins_api.notify import NotificationPayload
|
|
22
|
+
|
|
23
|
+
__all__ = ["ChannelPlugin", "PluginContext"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@runtime_checkable
|
|
27
|
+
class ChannelPlugin(Plugin, Protocol):
|
|
28
|
+
"""A plugin that can also receive ``NotificationPayload`` from the
|
|
29
|
+
framework's notifier (in addition to providing the standard CLI dispatch).
|
|
30
|
+
|
|
31
|
+
Implementations satisfy both the :class:`Plugin` contract (``metadata`` +
|
|
32
|
+
``validate_static`` + ``dispatch``) AND the channel-specific ``push`` hook.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
metadata: PluginMetadata
|
|
36
|
+
|
|
37
|
+
def push(self, ctx: PluginContext, payload: NotificationPayload) -> None:
|
|
38
|
+
"""Format payload and dispatch to the IM platform.
|
|
39
|
+
|
|
40
|
+
May raise — the framework's MultiplexNotifier catches and isolates
|
|
41
|
+
per-channel failures so one broken channel never blocks others.
|
|
42
|
+
"""
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Strategy event model.
|
|
2
|
+
|
|
3
|
+
DESIGN §8.5: full enumeration of EventType values. v0.5+: each plugin owns
|
|
4
|
+
its own ``<prefix>_events`` table (e.g. ``lub_events`` / ``va_events``) and
|
|
5
|
+
decides whether/how to persist these — the framework no longer provides a
|
|
6
|
+
unified ``strategy_events`` table or a runner that writes into one.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from enum import StrEnum
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EventType(StrEnum):
|
|
18
|
+
# step lifecycle
|
|
19
|
+
STEP_STARTED = "step.started"
|
|
20
|
+
STEP_PROGRESS = "step.progress"
|
|
21
|
+
STEP_FINISHED = "step.finished"
|
|
22
|
+
# data sync
|
|
23
|
+
DATA_SYNC_STARTED = "data.sync.started"
|
|
24
|
+
DATA_SYNC_FINISHED = "data.sync.finished"
|
|
25
|
+
# tushare
|
|
26
|
+
TUSHARE_CALL = "tushare.call"
|
|
27
|
+
TUSHARE_FALLBACK = "tushare.fallback"
|
|
28
|
+
TUSHARE_UNAUTH = "tushare.unauthorized"
|
|
29
|
+
# llm
|
|
30
|
+
LLM_BATCH_STARTED = "llm.batch.started"
|
|
31
|
+
LLM_BATCH_FINISHED = "llm.batch.finished"
|
|
32
|
+
LLM_FINAL_RANK = "llm.final_ranking"
|
|
33
|
+
VALIDATION_FAILED = "validation.failed"
|
|
34
|
+
# result
|
|
35
|
+
RESULT_PERSISTED = "result.persisted"
|
|
36
|
+
LOG = "log"
|
|
37
|
+
# Live row content — plugin-driven dashboard "current phase" message.
|
|
38
|
+
# The dashboard's Live row reads `event.message` of LIVE_STATUS events
|
|
39
|
+
# verbatim and shows nothing else there. The framework deliberately does
|
|
40
|
+
# NOT parse any other event's message string to infer phase, so plugin
|
|
41
|
+
# wording stays decoupled from framework code. Optional payload key
|
|
42
|
+
# ``terminal=True`` marks the emit as the run's final-state text so
|
|
43
|
+
# the framework's mark_finished default doesn't overwrite it.
|
|
44
|
+
LIVE_STATUS = "live.status"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class EventLevel(StrEnum):
|
|
48
|
+
INFO = "info"
|
|
49
|
+
WARN = "warn"
|
|
50
|
+
ERROR = "error"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class StrategyEvent(BaseModel):
|
|
54
|
+
"""Single event emitted by a strategy plugin."""
|
|
55
|
+
|
|
56
|
+
model_config = ConfigDict(extra="forbid")
|
|
57
|
+
|
|
58
|
+
type: EventType
|
|
59
|
+
level: EventLevel = EventLevel.INFO
|
|
60
|
+
message: str
|
|
61
|
+
payload: dict[str, Any] = Field(default_factory=dict)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""LLM调参契约 — 插件作者唯一需要 import 的 LLM 调参类型。
|
|
2
|
+
|
|
3
|
+
v0.7:Stage 概念彻底退出框架。框架的 ``LLMClient.complete_json`` 不再认识
|
|
4
|
+
stage 名字,由调用方直接传入一个已解析好的 ``StageProfile`` 实例;预设档名
|
|
5
|
+
(``fast / balanced / quality``)保留为框架级用户配置(``app.profile``),
|
|
6
|
+
但**预设档 → 各 stage tuning 的映射表归插件自己维护**。
|
|
7
|
+
|
|
8
|
+
插件作者用法:
|
|
9
|
+
|
|
10
|
+
from deeptrade.plugins_api import StageProfile
|
|
11
|
+
|
|
12
|
+
PROFILES: dict[str, dict[str, StageProfile]] = {
|
|
13
|
+
"fast": {"my_stage": StageProfile(thinking=False, ...)},
|
|
14
|
+
"balanced": {"my_stage": StageProfile(thinking=True, ...)},
|
|
15
|
+
"quality": {"my_stage": StageProfile(thinking=True, ...)},
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
def resolve_profile(preset: str, stage: str) -> StageProfile:
|
|
19
|
+
return PROFILES[preset][stage]
|
|
20
|
+
|
|
21
|
+
随后在 pipeline 中:
|
|
22
|
+
|
|
23
|
+
prof = resolve_profile(rt.config.get_app_config().app_profile, "my_stage")
|
|
24
|
+
obj, _ = llm.complete_json(system=..., user=..., schema=..., profile=prof)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from typing import Literal
|
|
30
|
+
|
|
31
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class StageProfile(BaseModel):
|
|
35
|
+
"""单次 LLM 调用的调参档:四字段对应 OpenAI Chat Completions 协议参数。
|
|
36
|
+
|
|
37
|
+
``thinking`` / ``reasoning_effort`` 在不支持思维链的 provider 上由 transport
|
|
38
|
+
静默丢弃(DeepSeek/Qwen/Kimi/Doubao/GLM/... 各家支持情况不同)。
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
model_config = ConfigDict(extra="forbid")
|
|
42
|
+
|
|
43
|
+
thinking: bool
|
|
44
|
+
reasoning_effort: Literal["low", "medium", "high"]
|
|
45
|
+
temperature: float = Field(ge=0.0, le=2.0)
|
|
46
|
+
max_output_tokens: int = Field(ge=1024, le=384_000)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Plugin metadata schema.
|
|
2
|
+
|
|
3
|
+
DESIGN §8.2 (v0.3.1):
|
|
4
|
+
* `tables` declares table names + purge policy ONLY (no inline DDL).
|
|
5
|
+
* `migrations` is the SOLE DDL execution path (S1 fix).
|
|
6
|
+
* `permissions.llm_tools` is fixed False (M3 hard constraint).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Literal
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TableSpec(BaseModel):
|
|
17
|
+
"""Table the plugin owns. DDL is in migrations/, NOT here."""
|
|
18
|
+
|
|
19
|
+
model_config = ConfigDict(extra="forbid")
|
|
20
|
+
name: str = Field(..., pattern=r"^[a-z][a-z0-9_]{1,63}$")
|
|
21
|
+
description: str = ""
|
|
22
|
+
purge_on_uninstall: bool = True
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MigrationSpec(BaseModel):
|
|
26
|
+
"""One SQL migration file. Filename version pattern: 'YYYYMMDD_NNN'."""
|
|
27
|
+
|
|
28
|
+
model_config = ConfigDict(extra="forbid")
|
|
29
|
+
version: str = Field(..., pattern=r"^\d{8}_\d{3,}$")
|
|
30
|
+
file: str
|
|
31
|
+
checksum: str = Field(..., pattern=r"^sha256:[0-9a-f]{64}$")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TushareApiPermissions(BaseModel):
|
|
35
|
+
model_config = ConfigDict(extra="forbid")
|
|
36
|
+
required: list[str] = Field(default_factory=list)
|
|
37
|
+
optional: list[str] = Field(default_factory=list)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class PluginPermissions(BaseModel):
|
|
41
|
+
model_config = ConfigDict(extra="forbid")
|
|
42
|
+
tushare_apis: TushareApiPermissions = Field(default_factory=TushareApiPermissions)
|
|
43
|
+
llm: bool = False
|
|
44
|
+
# M3 hard constraint: any non-False value is rejected at install time.
|
|
45
|
+
llm_tools: Literal[False] = False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class PluginMetadata(BaseModel):
|
|
49
|
+
"""Top-level plugin descriptor parsed from deeptrade_plugin.yaml."""
|
|
50
|
+
|
|
51
|
+
model_config = ConfigDict(extra="forbid")
|
|
52
|
+
|
|
53
|
+
plugin_id: str = Field(..., pattern=r"^[a-z][a-z0-9-]{2,31}$")
|
|
54
|
+
name: str
|
|
55
|
+
version: str
|
|
56
|
+
# v0.4: widened to support channel plugins (DESIGN §18.4). Old yaml with
|
|
57
|
+
# type='strategy' (or omitted) remains valid via the default.
|
|
58
|
+
type: Literal["strategy", "channel"] = "strategy"
|
|
59
|
+
api_version: str
|
|
60
|
+
entrypoint: str = Field(..., pattern=r"^[A-Za-z_][\w\.]*:[A-Za-z_]\w*$")
|
|
61
|
+
description: str
|
|
62
|
+
author: str = ""
|
|
63
|
+
permissions: PluginPermissions = Field(default_factory=PluginPermissions)
|
|
64
|
+
tables: list[TableSpec]
|
|
65
|
+
migrations: list[MigrationSpec]
|
|
66
|
+
|
|
67
|
+
@model_validator(mode="after")
|
|
68
|
+
def _migrations_not_empty(self) -> PluginMetadata:
|
|
69
|
+
if not self.migrations:
|
|
70
|
+
raise ValueError(
|
|
71
|
+
"metadata.migrations cannot be empty; DDL must be managed via migrations/*.sql"
|
|
72
|
+
)
|
|
73
|
+
# version uniqueness
|
|
74
|
+
versions = [m.version for m in self.migrations]
|
|
75
|
+
if len(versions) != len(set(versions)):
|
|
76
|
+
raise ValueError("duplicate migration versions in metadata.migrations")
|
|
77
|
+
return self
|
|
78
|
+
|
|
79
|
+
@model_validator(mode="after")
|
|
80
|
+
def _tables_unique(self) -> PluginMetadata:
|
|
81
|
+
names = [t.name for t in self.tables]
|
|
82
|
+
if len(names) != len(set(names)):
|
|
83
|
+
raise ValueError("duplicate table names in metadata.tables")
|
|
84
|
+
return self
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""IM notification payload — strategy plugin → notifier contract.
|
|
2
|
+
|
|
3
|
+
DESIGN §18.3: payload carries STRUCTURED SEMANTIC DATA only (no pre-rendered
|
|
4
|
+
markdown). Channel plugins are responsible for format conversion (markdown for
|
|
5
|
+
feishu/dingtalk, plain text for SMS, interactive blocks for slack, ...).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
13
|
+
|
|
14
|
+
from deeptrade.core.run_status import RunStatus
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class NotificationItem(BaseModel):
|
|
18
|
+
"""One semantic row (a stock pick, a watchlist entry, ...).
|
|
19
|
+
|
|
20
|
+
Channels render this as a list row, card field, or a bullet — whichever
|
|
21
|
+
fits their format. ``fields`` carries channel-agnostic extras the channel
|
|
22
|
+
may surface if it has the space.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
model_config = ConfigDict(extra="forbid")
|
|
26
|
+
|
|
27
|
+
code: str
|
|
28
|
+
name: str | None = None
|
|
29
|
+
rank: int | None = None
|
|
30
|
+
score: float | None = None
|
|
31
|
+
label: str | None = None
|
|
32
|
+
note: str | None = None
|
|
33
|
+
fields: dict[str, str | int | float] = Field(default_factory=dict)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class NotificationSection(BaseModel):
|
|
37
|
+
"""A semantically related group of items (e.g. 'top_candidates' / 'watchlist').
|
|
38
|
+
|
|
39
|
+
``key`` is the stable machine-readable identifier; ``title`` is what
|
|
40
|
+
channels display.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
model_config = ConfigDict(extra="forbid")
|
|
44
|
+
|
|
45
|
+
key: str
|
|
46
|
+
title: str
|
|
47
|
+
items: list[NotificationItem]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class NotificationPayload(BaseModel):
|
|
51
|
+
"""Everything a strategy plugin hands to the notifier in one shot.
|
|
52
|
+
|
|
53
|
+
Channels MUST tolerate empty ``sections`` / ``metrics`` (degraded modes
|
|
54
|
+
like SMS only get ``title`` + ``summary``).
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
model_config = ConfigDict(extra="forbid")
|
|
58
|
+
|
|
59
|
+
plugin_id: str
|
|
60
|
+
run_id: str
|
|
61
|
+
status: RunStatus
|
|
62
|
+
title: str
|
|
63
|
+
summary: str
|
|
64
|
+
sections: list[NotificationSection] = Field(default_factory=list)
|
|
65
|
+
metrics: dict[str, str | int | float] = Field(default_factory=dict)
|
|
66
|
+
report_dir: str | None = None
|
|
67
|
+
extras: dict[str, Any] = Field(default_factory=dict)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
plugin_id: limit-up-board
|
|
2
|
+
name: 打板策略
|
|
3
|
+
version: 0.4.0
|
|
4
|
+
type: strategy
|
|
5
|
+
api_version: "1"
|
|
6
|
+
entrypoint: limit_up_board.plugin:LimitUpBoardPlugin
|
|
7
|
+
description: A 股打板策略:双轮 LLM 漏斗(强势标的分析 → 连板预测)
|
|
8
|
+
author: DeepTrade
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
tushare_apis:
|
|
12
|
+
required:
|
|
13
|
+
- stock_basic
|
|
14
|
+
- trade_cal
|
|
15
|
+
- daily
|
|
16
|
+
- daily_basic
|
|
17
|
+
- stock_st
|
|
18
|
+
- limit_list_d
|
|
19
|
+
- limit_step
|
|
20
|
+
- moneyflow
|
|
21
|
+
- top_list
|
|
22
|
+
- top_inst
|
|
23
|
+
- cyq_perf
|
|
24
|
+
optional:
|
|
25
|
+
- limit_list_ths
|
|
26
|
+
- limit_cpt_list
|
|
27
|
+
- ths_hot
|
|
28
|
+
- dc_hot
|
|
29
|
+
- stk_auction_o
|
|
30
|
+
- anns_d
|
|
31
|
+
- suspend_d
|
|
32
|
+
- stk_limit
|
|
33
|
+
llm: true
|
|
34
|
+
llm_tools: false
|
|
35
|
+
|
|
36
|
+
migrations:
|
|
37
|
+
- version: "20260430_001"
|
|
38
|
+
file: migrations/20260430_001_init.sql
|
|
39
|
+
checksum: "sha256:6009465242554848315fcd058b816829161f1f51803db004361995ddf28814c3"
|
|
40
|
+
- version: "20260501_002"
|
|
41
|
+
file: migrations/20260501_002_lub_stage_results_llm_provider.sql
|
|
42
|
+
checksum: "sha256:9db92478a786984b69fa37a143b806357f940bd2471253d7bd4918735c41690f"
|
|
43
|
+
- version: "20260508_001"
|
|
44
|
+
file: migrations/20260508_001_lub_lhb_tables.sql
|
|
45
|
+
checksum: "sha256:12e32fa078874c1b7d30645646a23e345db2937f00534269d282251c9b497f0d"
|
|
46
|
+
- version: "20260508_002"
|
|
47
|
+
file: migrations/20260508_002_lub_cyq_perf.sql
|
|
48
|
+
checksum: "sha256:5558169e6c5f6ae72be026dfac598f9a5d886d01b76a53565796a6f1b5665772"
|
|
49
|
+
- version: "20260508_003"
|
|
50
|
+
file: migrations/20260508_003_lub_lhb_pk_fix.sql
|
|
51
|
+
checksum: "sha256:a5efaf93f1399131610e2d0ee7b23bb00846a0aec72801589b91d6669443f187"
|
|
52
|
+
- version: "20260508_004"
|
|
53
|
+
file: migrations/20260508_004_lub_lhb_drop_pk.sql
|
|
54
|
+
checksum: "sha256:192837aa88f31b985575d73840a516719cf692ae1be0dce447fcd98d0de0667f"
|
|
55
|
+
- version: "20260508_005"
|
|
56
|
+
file: migrations/20260508_005_lub_config.sql
|
|
57
|
+
checksum: "sha256:5971f06666ee96a23a5297290e8bc725dab61debbb64a58925abcc26d8844a3c"
|
|
58
|
+
|
|
59
|
+
tables:
|
|
60
|
+
- name: lub_stock_basic
|
|
61
|
+
description: stock_basic 落库(限本插件使用)
|
|
62
|
+
purge_on_uninstall: true
|
|
63
|
+
- name: lub_trade_cal
|
|
64
|
+
description: trade_cal 落库
|
|
65
|
+
purge_on_uninstall: true
|
|
66
|
+
- name: lub_daily
|
|
67
|
+
description: daily 行情落库
|
|
68
|
+
purge_on_uninstall: true
|
|
69
|
+
- name: lub_daily_basic
|
|
70
|
+
description: daily_basic 行情指标落库
|
|
71
|
+
purge_on_uninstall: true
|
|
72
|
+
- name: lub_moneyflow
|
|
73
|
+
description: moneyflow 资金流落库
|
|
74
|
+
purge_on_uninstall: true
|
|
75
|
+
- name: lub_limit_list_d
|
|
76
|
+
description: 涨停明细缓存(limit_list_d)
|
|
77
|
+
purge_on_uninstall: true
|
|
78
|
+
- name: lub_limit_ths
|
|
79
|
+
description: 同花顺涨停榜单缓存(limit_list_ths)
|
|
80
|
+
purge_on_uninstall: true
|
|
81
|
+
- name: lub_stage_results
|
|
82
|
+
description: 策略各阶段(R1/R2/final_ranking)结构化结果
|
|
83
|
+
purge_on_uninstall: true
|
|
84
|
+
- name: lub_runs
|
|
85
|
+
description: 本插件 run 历史(替代框架 strategy_runs)
|
|
86
|
+
purge_on_uninstall: true
|
|
87
|
+
- name: lub_events
|
|
88
|
+
description: 本插件 run 事件流(替代框架 strategy_events)
|
|
89
|
+
purge_on_uninstall: true
|
|
90
|
+
- name: lub_top_list
|
|
91
|
+
description: 龙虎榜个股明细(top_list)
|
|
92
|
+
purge_on_uninstall: true
|
|
93
|
+
- name: lub_top_inst
|
|
94
|
+
description: 龙虎榜机构席位明细(top_inst)
|
|
95
|
+
purge_on_uninstall: true
|
|
96
|
+
- name: lub_cyq_perf
|
|
97
|
+
description: 筹码每日指标(cyq_perf)
|
|
98
|
+
purge_on_uninstall: true
|
|
99
|
+
- name: lub_config
|
|
100
|
+
description: 本插件用户可调设置(流通市值 / 股价上限)
|
|
101
|
+
purge_on_uninstall: true
|
|
File without changes
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""TradeCalendar — pure helper around tushare trade_cal frames."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TradeCalendar:
|
|
9
|
+
"""Read-only view over the trade_cal DataFrame.
|
|
10
|
+
|
|
11
|
+
Required columns: cal_date (str YYYYMMDD), is_open (0/1), pretrade_date (str).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, df: pd.DataFrame) -> None:
|
|
15
|
+
if not {"cal_date", "is_open"}.issubset(df.columns):
|
|
16
|
+
raise ValueError("trade_cal frame missing required columns")
|
|
17
|
+
|
|
18
|
+
# ⚠ Bug fix: tushare trade_cal returns cal_date as int64 in some
|
|
19
|
+
# environments; JSON cache round-trip can also widen "20260428" → 20260428.
|
|
20
|
+
# Normalize to string up-front so all downstream comparisons are str↔str.
|
|
21
|
+
df = df.copy()
|
|
22
|
+
df["cal_date"] = df["cal_date"].astype(str)
|
|
23
|
+
if "pretrade_date" in df.columns:
|
|
24
|
+
# Some non-trading days have NaN pretrade_date; preserve NaN, stringify the rest
|
|
25
|
+
df["pretrade_date"] = df["pretrade_date"].apply(
|
|
26
|
+
lambda v: str(v) if pd.notna(v) else None
|
|
27
|
+
)
|
|
28
|
+
# is_open is sometimes "1"/"0" strings — coerce to int for ==1 comparison
|
|
29
|
+
df["is_open"] = pd.to_numeric(df["is_open"], errors="coerce").fillna(0).astype(int)
|
|
30
|
+
|
|
31
|
+
# Normalize: deduplicate on cal_date, keeping the SSE/last row
|
|
32
|
+
sorted_df = df.sort_values("cal_date").drop_duplicates("cal_date", keep="last")
|
|
33
|
+
self._df = sorted_df.reset_index(drop=True)
|
|
34
|
+
self._idx: dict[str, int] = {
|
|
35
|
+
str(row.cal_date): i for i, row in enumerate(self._df.itertuples(index=False))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
def is_open(self, date: str) -> bool:
|
|
39
|
+
idx = self._idx.get(date)
|
|
40
|
+
if idx is None:
|
|
41
|
+
return False
|
|
42
|
+
return int(self._df.at[idx, "is_open"]) == 1
|
|
43
|
+
|
|
44
|
+
def pretrade_date(self, date: str) -> str:
|
|
45
|
+
"""Return the most recent open day strictly BEFORE `date`."""
|
|
46
|
+
# Walk backwards through cal_date sorted index
|
|
47
|
+
candidates = self._df[self._df["cal_date"] < date]
|
|
48
|
+
opens = candidates[candidates["is_open"] == 1]
|
|
49
|
+
if opens.empty:
|
|
50
|
+
raise ValueError(f"no prior open trading day before {date}")
|
|
51
|
+
return str(opens.iloc[-1]["cal_date"])
|
|
52
|
+
|
|
53
|
+
def next_open(self, date: str) -> str:
|
|
54
|
+
"""Return the first open day strictly AFTER `date`."""
|
|
55
|
+
candidates = self._df[self._df["cal_date"] > date]
|
|
56
|
+
opens = candidates[candidates["is_open"] == 1]
|
|
57
|
+
if opens.empty:
|
|
58
|
+
raise ValueError(f"no future open trading day after {date}")
|
|
59
|
+
return str(opens.iloc[0]["cal_date"])
|
|
60
|
+
|
|
61
|
+
def latest_closed_on_or_before(self, date: str) -> str:
|
|
62
|
+
"""Return the latest open day ≤ `date` (i.e. `date` itself if open, else preceding)."""
|
|
63
|
+
if self.is_open(date):
|
|
64
|
+
return date
|
|
65
|
+
return self.pretrade_date(date)
|