deeptrade-quant 0.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. deeptrade/__init__.py +8 -0
  2. deeptrade/channels_builtin/__init__.py +0 -0
  3. deeptrade/channels_builtin/stdout/__init__.py +0 -0
  4. deeptrade/channels_builtin/stdout/deeptrade_plugin.yaml +25 -0
  5. deeptrade/channels_builtin/stdout/migrations/20260429_001_init.sql +13 -0
  6. deeptrade/channels_builtin/stdout/stdout_channel/__init__.py +0 -0
  7. deeptrade/channels_builtin/stdout/stdout_channel/channel.py +180 -0
  8. deeptrade/cli.py +214 -0
  9. deeptrade/cli_config.py +396 -0
  10. deeptrade/cli_data.py +33 -0
  11. deeptrade/cli_plugin.py +176 -0
  12. deeptrade/core/__init__.py +8 -0
  13. deeptrade/core/config.py +344 -0
  14. deeptrade/core/config_migrations.py +138 -0
  15. deeptrade/core/db.py +176 -0
  16. deeptrade/core/llm_client.py +591 -0
  17. deeptrade/core/llm_manager.py +174 -0
  18. deeptrade/core/logging_config.py +61 -0
  19. deeptrade/core/migrations/__init__.py +0 -0
  20. deeptrade/core/migrations/core/20260427_001_init.sql +121 -0
  21. deeptrade/core/migrations/core/20260501_002_drop_llm_calls_stage.sql +10 -0
  22. deeptrade/core/migrations/core/__init__.py +0 -0
  23. deeptrade/core/notifier.py +302 -0
  24. deeptrade/core/paths.py +49 -0
  25. deeptrade/core/plugin_manager.py +616 -0
  26. deeptrade/core/run_status.py +29 -0
  27. deeptrade/core/secrets.py +152 -0
  28. deeptrade/core/tushare_client.py +824 -0
  29. deeptrade/plugins_api/__init__.py +44 -0
  30. deeptrade/plugins_api/base.py +66 -0
  31. deeptrade/plugins_api/channel.py +42 -0
  32. deeptrade/plugins_api/events.py +61 -0
  33. deeptrade/plugins_api/llm.py +46 -0
  34. deeptrade/plugins_api/metadata.py +84 -0
  35. deeptrade/plugins_api/notify.py +67 -0
  36. deeptrade/strategies_builtin/__init__.py +0 -0
  37. deeptrade/strategies_builtin/limit_up_board/__init__.py +0 -0
  38. deeptrade/strategies_builtin/limit_up_board/deeptrade_plugin.yaml +101 -0
  39. deeptrade/strategies_builtin/limit_up_board/limit_up_board/__init__.py +0 -0
  40. deeptrade/strategies_builtin/limit_up_board/limit_up_board/calendar.py +65 -0
  41. deeptrade/strategies_builtin/limit_up_board/limit_up_board/cli.py +269 -0
  42. deeptrade/strategies_builtin/limit_up_board/limit_up_board/config.py +76 -0
  43. deeptrade/strategies_builtin/limit_up_board/limit_up_board/data.py +1191 -0
  44. deeptrade/strategies_builtin/limit_up_board/limit_up_board/pipeline.py +869 -0
  45. deeptrade/strategies_builtin/limit_up_board/limit_up_board/plugin.py +30 -0
  46. deeptrade/strategies_builtin/limit_up_board/limit_up_board/profiles.py +85 -0
  47. deeptrade/strategies_builtin/limit_up_board/limit_up_board/prompts.py +485 -0
  48. deeptrade/strategies_builtin/limit_up_board/limit_up_board/render.py +890 -0
  49. deeptrade/strategies_builtin/limit_up_board/limit_up_board/runner.py +1087 -0
  50. deeptrade/strategies_builtin/limit_up_board/limit_up_board/runtime.py +172 -0
  51. deeptrade/strategies_builtin/limit_up_board/limit_up_board/schemas.py +178 -0
  52. deeptrade/strategies_builtin/limit_up_board/migrations/20260430_001_init.sql +150 -0
  53. deeptrade/strategies_builtin/limit_up_board/migrations/20260501_002_lub_stage_results_llm_provider.sql +8 -0
  54. deeptrade/strategies_builtin/limit_up_board/migrations/20260508_001_lub_lhb_tables.sql +36 -0
  55. deeptrade/strategies_builtin/limit_up_board/migrations/20260508_002_lub_cyq_perf.sql +18 -0
  56. deeptrade/strategies_builtin/limit_up_board/migrations/20260508_003_lub_lhb_pk_fix.sql +46 -0
  57. deeptrade/strategies_builtin/limit_up_board/migrations/20260508_004_lub_lhb_drop_pk.sql +53 -0
  58. deeptrade/strategies_builtin/limit_up_board/migrations/20260508_005_lub_config.sql +17 -0
  59. deeptrade/strategies_builtin/volume_anomaly/__init__.py +0 -0
  60. deeptrade/strategies_builtin/volume_anomaly/deeptrade_plugin.yaml +59 -0
  61. deeptrade/strategies_builtin/volume_anomaly/migrations/20260430_001_init.sql +94 -0
  62. deeptrade/strategies_builtin/volume_anomaly/migrations/20260601_001_realized_returns.sql +44 -0
  63. deeptrade/strategies_builtin/volume_anomaly/migrations/20260601_002_dimension_scores.sql +13 -0
  64. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/__init__.py +0 -0
  65. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/calendar.py +52 -0
  66. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/cli.py +247 -0
  67. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/data.py +2154 -0
  68. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/pipeline.py +327 -0
  69. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/plugin.py +22 -0
  70. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/profiles.py +49 -0
  71. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/prompts.py +187 -0
  72. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/prompts_examples.py +84 -0
  73. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/render.py +906 -0
  74. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/runner.py +772 -0
  75. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/runtime.py +90 -0
  76. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/schemas.py +97 -0
  77. deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/stats.py +174 -0
  78. deeptrade/theme.py +48 -0
  79. deeptrade_quant-0.0.2.dist-info/METADATA +166 -0
  80. deeptrade_quant-0.0.2.dist-info/RECORD +83 -0
  81. deeptrade_quant-0.0.2.dist-info/WHEEL +4 -0
  82. deeptrade_quant-0.0.2.dist-info/entry_points.txt +2 -0
  83. deeptrade_quant-0.0.2.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,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
@@ -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
@@ -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)