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,172 @@
1
+ """LubRuntime — context bundle the plugin's pipeline runs against.
2
+
3
+ Replaces the old framework-provided ``StrategyContext``. The plugin owns its
4
+ own runtime now: it constructs db / config / tushare itself, and obtains LLM
5
+ clients on-demand from the framework's :class:`LLMManager`.
6
+
7
+ v0.6 — ``llm: DeepSeekClient`` field removed. ``llms: LLMManager`` is the
8
+ new framework hand-off; runner / pipeline pull a per-provider ``LLMClient``
9
+ via ``rt.llms.get_client(name, plugin_id=, run_id=)``.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ from dataclasses import dataclass
16
+ from typing import TYPE_CHECKING, Any
17
+
18
+ from deeptrade.plugins_api.events import EventLevel, EventType, StrategyEvent
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.core.llm_manager import LLMManager
24
+ from deeptrade.core.tushare_client import TushareClient
25
+ from deeptrade.plugins_api.notify import NotificationPayload
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ PLUGIN_ID = "limit-up-board"
30
+
31
+
32
+ @dataclass
33
+ class LubRuntime:
34
+ """Services bundle the plugin's run() / sync_data() use.
35
+
36
+ ``llms`` is the framework's LLMManager — call
37
+ ``rt.llms.get_client(name, plugin_id=rt.plugin_id, run_id=rt.run_id, ...)``
38
+ to obtain a per-provider client. The plugin may use multiple providers
39
+ in the same run.
40
+ """
41
+
42
+ db: Database
43
+ config: ConfigService
44
+ llms: LLMManager
45
+ plugin_id: str = PLUGIN_ID
46
+ run_id: str | None = None
47
+ is_intraday: bool = False
48
+ tushare: TushareClient | None = None
49
+
50
+ def emit(
51
+ self,
52
+ event_type: EventType,
53
+ message: str,
54
+ *,
55
+ level: EventLevel = EventLevel.INFO,
56
+ **payload: object,
57
+ ) -> StrategyEvent:
58
+ return StrategyEvent(type=event_type, level=level, message=message, payload=dict(payload))
59
+
60
+ def notify(self, payload: NotificationPayload) -> bool:
61
+ """Push a NotificationPayload through the framework's notifier.
62
+
63
+ Returns True on success, False if no channel is enabled or dispatch
64
+ raised. Never blocks on HTTP — top-level ``deeptrade.notify`` builds
65
+ a notifier that uses an async dispatch worker.
66
+ """
67
+ from deeptrade import notify as _notify
68
+
69
+ try:
70
+ _notify(self.db, payload)
71
+ return True
72
+ except Exception as e: # noqa: BLE001
73
+ logger.warning("notify dispatch failed: %s", e)
74
+ return False
75
+
76
+ def is_notify_enabled(self) -> bool:
77
+ """Cheap probe: any channel plugin enabled?"""
78
+ from deeptrade.core.plugin_manager import PluginManager
79
+
80
+ mgr = PluginManager(self.db)
81
+ return any(r.type == "channel" and r.enabled for r in mgr.list_all())
82
+
83
+
84
+ def build_tushare_client(
85
+ rt: LubRuntime,
86
+ *,
87
+ intraday: bool = False,
88
+ event_cb: Any = None,
89
+ ) -> TushareClient:
90
+ """Construct a TushareClient bound to this plugin."""
91
+ from deeptrade.core.tushare_client import TushareClient, TushareSDKTransport
92
+
93
+ token = rt.config.get("tushare.token")
94
+ if not token:
95
+ raise RuntimeError("tushare.token not configured; run `deeptrade config set-tushare`")
96
+ cfg = rt.config.get_app_config()
97
+ transport = TushareSDKTransport(str(token))
98
+ return TushareClient(
99
+ rt.db,
100
+ transport,
101
+ plugin_id=rt.plugin_id,
102
+ rps=cfg.tushare_rps,
103
+ intraday=intraday,
104
+ event_cb=event_cb,
105
+ )
106
+
107
+
108
+ def open_worker_runtime(
109
+ plugin_id: str,
110
+ run_id: str,
111
+ *,
112
+ config: ConfigService,
113
+ is_intraday: bool = False,
114
+ ) -> tuple[Database, LubRuntime]:
115
+ """Construct an isolated runtime for a debate-mode worker thread.
116
+
117
+ Each worker gets its own DuckDB connection + ``LLMManager`` so that
118
+ concurrent ``LLMClient.complete_json`` calls don't share the lock /
119
+ audit-write bookkeeping. Same-process multiple ``duckdb.connect()`` calls
120
+ against the same file share the underlying DB instance, so writes still
121
+ land in the same physical file (run history visible across all workers).
122
+
123
+ The ``ConfigService`` (and its ``SecretStore``) is **shared** with the
124
+ main thread on purpose: ``SecretStore`` probes the OS keyring at
125
+ construction time with a side-effecting set/get/delete round-trip, and
126
+ running that probe per worker is both wasteful and racy — concurrent
127
+ workers overwrite each other's probe key, causing false negatives that
128
+ silently demote keyring-stored secrets to "no api_key set".
129
+
130
+ Sharing implies that ``ConfigService`` reads (e.g. ``get_app_config``)
131
+ are issued against the **main thread's** ``Database._conn`` from worker
132
+ threads. ``Database.fetchone`` / ``fetchall`` MUST therefore hold their
133
+ write lock across the full execute+fetch round-trip; otherwise concurrent
134
+ workers race on the connection's result set and trigger a native heap
135
+ corruption (Windows 0xC0000374). See ``deeptrade.core.db``.
136
+
137
+ The worker MUST close ``db`` when done.
138
+ """
139
+ from deeptrade.core import paths
140
+ from deeptrade.core.db import Database
141
+ from deeptrade.core.llm_manager import LLMManager
142
+
143
+ db = Database(paths.db_path())
144
+ rt = LubRuntime(
145
+ db=db,
146
+ config=config,
147
+ llms=LLMManager(db, config),
148
+ plugin_id=plugin_id,
149
+ run_id=run_id,
150
+ is_intraday=is_intraday,
151
+ )
152
+ return db, rt
153
+
154
+
155
+ def pick_llm_provider(rt: LubRuntime) -> str:
156
+ """Pick which configured LLM provider to use for this run.
157
+
158
+ v0.6 policy: prefer ``deepseek`` (the original default and the target of
159
+ the legacy-config auto-migration), else fall back to the first available
160
+ provider. v0.7 will let the user override via a plugin-level config key
161
+ such as ``limit-up-board.default_llm``.
162
+
163
+ Raises ``RuntimeError`` if no provider is configured at all.
164
+ """
165
+ available = rt.llms.list_providers()
166
+ if not available:
167
+ raise RuntimeError(
168
+ "No LLM provider configured; run `deeptrade config set-llm`"
169
+ )
170
+ if "deepseek" in available:
171
+ return "deepseek"
172
+ return available[0]
@@ -0,0 +1,178 @@
1
+ """Pydantic schemas for the LLM stages.
2
+
3
+ DESIGN §12.4-12.5 + the v0.3.1 fixes:
4
+ F5 — R1 evidence max_length=4 (was 8); rationale length-capped via prompt
5
+ M3 — extra='forbid' on every model
6
+ M4 — FinalRankingResponse for multi-batch R2 reconciliation
7
+ S5 — final_rank field separated from batch_local_rank semantics
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Literal
13
+
14
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
15
+
16
+ # ---------------------------------------------------------------------------
17
+ # Common evidence shape
18
+ # ---------------------------------------------------------------------------
19
+
20
+
21
+ class EvidenceItem(BaseModel):
22
+ """One field-level fact from the input data the LLM is using to reason.
23
+
24
+ `field` MUST refer to a key actually present in the input prompt (e.g.
25
+ `fd_amount_yi`, `up_stat`); `unit` is REQUIRED (F-M1 fix) so the prompt
26
+ and the LLM speak the same units (亿/万/%/次/日/秒/none/...).
27
+ Use literal "none" when the field genuinely has no unit (e.g. categorical).
28
+ """
29
+
30
+ model_config = ConfigDict(extra="forbid")
31
+ field: str = Field(..., min_length=1, max_length=64)
32
+ value: str | int | float | None
33
+ unit: str = Field(..., min_length=1, max_length=16)
34
+ interpretation: str = Field(..., min_length=1, max_length=120)
35
+
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # R1 — strong-target analysis
39
+ # ---------------------------------------------------------------------------
40
+
41
+
42
+ class StrongCandidate(BaseModel):
43
+ """One R1 verdict per input candidate."""
44
+
45
+ model_config = ConfigDict(extra="forbid")
46
+ candidate_id: str
47
+ ts_code: str
48
+ name: str
49
+ selected: bool
50
+ score: float = Field(ge=0, le=100)
51
+ strength_level: Literal["high", "medium", "low"]
52
+ rationale: str = Field(..., max_length=120)
53
+ evidence: list[EvidenceItem] = Field(min_length=1, max_length=4)
54
+ risk_flags: list[str] = Field(default_factory=list, max_length=5)
55
+ missing_data: list[str] = Field(default_factory=list)
56
+
57
+
58
+ class StrongAnalysisResponse(BaseModel):
59
+ model_config = ConfigDict(extra="forbid")
60
+ stage: Literal["strong_target_analysis"]
61
+ trade_date: str
62
+ batch_no: int = Field(ge=1)
63
+ batch_total: int = Field(ge=1)
64
+ candidates: list[StrongCandidate]
65
+ batch_summary: str
66
+
67
+
68
+ # ---------------------------------------------------------------------------
69
+ # R2 — continuation prediction
70
+ # ---------------------------------------------------------------------------
71
+
72
+
73
+ class ContinuationCandidate(BaseModel):
74
+ model_config = ConfigDict(extra="forbid")
75
+ candidate_id: str
76
+ ts_code: str
77
+ name: str
78
+ rank: int = Field(ge=1)
79
+ continuation_score: float = Field(ge=0, le=100)
80
+ confidence: Literal["high", "medium", "low"]
81
+ prediction: Literal["top_candidate", "watchlist", "avoid"]
82
+ rationale: str = Field(..., max_length=200)
83
+ key_evidence: list[EvidenceItem] = Field(min_length=1, max_length=5)
84
+ next_day_watch_points: list[str] = Field(min_length=1, max_length=4)
85
+ failure_triggers: list[str] = Field(min_length=1, max_length=4)
86
+ missing_data: list[str] = Field(default_factory=list)
87
+
88
+
89
+ class ContinuationResponse(BaseModel):
90
+ model_config = ConfigDict(extra="forbid")
91
+ stage: Literal["limit_up_continuation_prediction"]
92
+ trade_date: str
93
+ next_trade_date: str
94
+ market_context_summary: str
95
+ risk_disclaimer: str
96
+ candidates: list[ContinuationCandidate]
97
+
98
+ @field_validator("candidates")
99
+ @classmethod
100
+ def ranks_must_be_dense_1_to_n(
101
+ cls, v: list[ContinuationCandidate]
102
+ ) -> list[ContinuationCandidate]:
103
+ """F-M1 — ranks must be a dense permutation 1..N (not just unique).
104
+ E.g. [1,2,3] OK; [1,3,5] or [10,20,30] rejected."""
105
+ ranks = sorted(c.rank for c in v)
106
+ expected = list(range(1, len(ranks) + 1))
107
+ if ranks != expected:
108
+ raise ValueError(f"candidate ranks must be a dense permutation 1..N; got {ranks}")
109
+ return v
110
+
111
+
112
+ # ---------------------------------------------------------------------------
113
+ # R3 — Debate-mode revision (each LLM revises its own R2 after seeing peers)
114
+ # ---------------------------------------------------------------------------
115
+
116
+
117
+ class RevisedContinuationCandidate(ContinuationCandidate):
118
+ """R2 fields + ``revision_note`` recording why the prediction shifted
119
+ after reviewing peer LLM outputs."""
120
+
121
+ model_config = ConfigDict(extra="forbid")
122
+ revision_note: str = Field(..., max_length=120)
123
+
124
+
125
+ class RevisionResponse(BaseModel):
126
+ """R3 output. ``candidates`` keeps the same candidate_id set as the LLM's
127
+ own R2 (set-equality enforced by the pipeline); ranks are 1..N dense
128
+ within this single batch."""
129
+
130
+ model_config = ConfigDict(extra="forbid")
131
+ stage: Literal["limit_up_continuation_revision"]
132
+ trade_date: str
133
+ next_trade_date: str
134
+ revision_summary: str = Field(..., max_length=200)
135
+ candidates: list[RevisedContinuationCandidate]
136
+
137
+ @field_validator("candidates")
138
+ @classmethod
139
+ def ranks_must_be_dense_1_to_n(
140
+ cls, v: list[RevisedContinuationCandidate]
141
+ ) -> list[RevisedContinuationCandidate]:
142
+ ranks = sorted(c.rank for c in v)
143
+ expected = list(range(1, len(ranks) + 1))
144
+ if ranks != expected:
145
+ raise ValueError(f"candidate ranks must be a dense permutation 1..N; got {ranks}")
146
+ return v
147
+
148
+
149
+ # ---------------------------------------------------------------------------
150
+ # Final-ranking — global re-rank when R2 was multi-batch (M4 + S5)
151
+ # ---------------------------------------------------------------------------
152
+
153
+
154
+ class FinalRankItem(BaseModel):
155
+ model_config = ConfigDict(extra="forbid")
156
+ candidate_id: str
157
+ ts_code: str
158
+ final_rank: int = Field(ge=1)
159
+ final_prediction: Literal["top_candidate", "watchlist", "avoid"]
160
+ final_confidence: Literal["high", "medium", "low"]
161
+ reason_vs_peers: str = Field(..., max_length=200)
162
+ delta_vs_batch: Literal["upgraded", "kept", "downgraded"]
163
+
164
+
165
+ class FinalRankingResponse(BaseModel):
166
+ model_config = ConfigDict(extra="forbid")
167
+ stage: Literal["final_ranking"]
168
+ trade_date: str
169
+ next_trade_date: str
170
+ finalists: list[FinalRankItem]
171
+
172
+ @field_validator("finalists")
173
+ @classmethod
174
+ def ranks_dense_and_unique(cls, v: list[FinalRankItem]) -> list[FinalRankItem]:
175
+ ranks = sorted(c.final_rank for c in v)
176
+ if ranks != list(range(1, len(ranks) + 1)):
177
+ raise ValueError("final_rank must be a dense permutation 1..N")
178
+ return v
@@ -0,0 +1,150 @@
1
+ -- limit-up-board strategy: full plugin schema (Plan A pure isolation).
2
+ --
3
+ -- All tables are plugin-owned. Tushare-derived "shared" tables (formerly in
4
+ -- core migrations) now live here under the lub_* prefix so the plugin owns
5
+ -- its own data in line with the no-cross-plugin-coupling principle.
6
+
7
+ -- ============================================================
8
+ -- Tushare-derived business tables (plugin-prefixed copies of what used to
9
+ -- be in core under the un-prefixed names)
10
+ -- ============================================================
11
+
12
+ CREATE TABLE IF NOT EXISTS lub_stock_basic (
13
+ ts_code VARCHAR PRIMARY KEY,
14
+ symbol VARCHAR, name VARCHAR, area VARCHAR, industry VARCHAR,
15
+ market VARCHAR, exchange VARCHAR,
16
+ list_status VARCHAR, list_date VARCHAR, delist_date VARCHAR,
17
+ is_hs VARCHAR, act_name VARCHAR, act_ent_type VARCHAR,
18
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
19
+ );
20
+
21
+ CREATE TABLE IF NOT EXISTS lub_trade_cal (
22
+ exchange VARCHAR, cal_date VARCHAR, is_open INTEGER, pretrade_date VARCHAR,
23
+ PRIMARY KEY (exchange, cal_date)
24
+ );
25
+
26
+ CREATE TABLE IF NOT EXISTS lub_daily (
27
+ ts_code VARCHAR, trade_date VARCHAR,
28
+ open DOUBLE, high DOUBLE, low DOUBLE, close DOUBLE, pre_close DOUBLE,
29
+ change DOUBLE, pct_chg DOUBLE, vol DOUBLE, amount DOUBLE,
30
+ PRIMARY KEY (ts_code, trade_date)
31
+ );
32
+
33
+ CREATE TABLE IF NOT EXISTS lub_daily_basic (
34
+ ts_code VARCHAR, trade_date VARCHAR, close DOUBLE,
35
+ turnover_rate DOUBLE, turnover_rate_f DOUBLE, volume_ratio DOUBLE,
36
+ pe DOUBLE, pe_ttm DOUBLE, pb DOUBLE, ps DOUBLE, ps_ttm DOUBLE,
37
+ total_share DOUBLE, float_share DOUBLE, free_share DOUBLE,
38
+ total_mv DOUBLE, circ_mv DOUBLE,
39
+ PRIMARY KEY (ts_code, trade_date)
40
+ );
41
+
42
+ CREATE TABLE IF NOT EXISTS lub_moneyflow (
43
+ ts_code VARCHAR,
44
+ trade_date VARCHAR,
45
+ buy_sm_vol DOUBLE, buy_sm_amount DOUBLE,
46
+ sell_sm_vol DOUBLE, sell_sm_amount DOUBLE,
47
+ buy_md_vol DOUBLE, buy_md_amount DOUBLE,
48
+ sell_md_vol DOUBLE, sell_md_amount DOUBLE,
49
+ buy_lg_vol DOUBLE, buy_lg_amount DOUBLE,
50
+ sell_lg_vol DOUBLE, sell_lg_amount DOUBLE,
51
+ buy_elg_vol DOUBLE, buy_elg_amount DOUBLE,
52
+ sell_elg_vol DOUBLE, sell_elg_amount DOUBLE,
53
+ net_mf_vol DOUBLE, net_mf_amount DOUBLE,
54
+ PRIMARY KEY (ts_code, trade_date)
55
+ );
56
+
57
+ -- ============================================================
58
+ -- Strategy-specific business tables
59
+ -- ============================================================
60
+
61
+ CREATE TABLE IF NOT EXISTS lub_limit_list_d (
62
+ trade_date VARCHAR NOT NULL,
63
+ ts_code VARCHAR NOT NULL,
64
+ name VARCHAR,
65
+ industry VARCHAR,
66
+ close DOUBLE,
67
+ pct_chg DOUBLE,
68
+ amount DOUBLE,
69
+ fd_amount DOUBLE,
70
+ limit_amount DOUBLE,
71
+ float_mv DOUBLE,
72
+ total_mv DOUBLE,
73
+ turnover_ratio DOUBLE,
74
+ first_time VARCHAR,
75
+ last_time VARCHAR,
76
+ open_times INTEGER,
77
+ up_stat VARCHAR,
78
+ limit_times INTEGER,
79
+ "limit" VARCHAR NOT NULL,
80
+ PRIMARY KEY (trade_date, ts_code, "limit")
81
+ );
82
+
83
+ CREATE TABLE IF NOT EXISTS lub_limit_ths (
84
+ trade_date VARCHAR NOT NULL,
85
+ ts_code VARCHAR NOT NULL,
86
+ name VARCHAR,
87
+ price DOUBLE,
88
+ pct_chg DOUBLE,
89
+ open_num INTEGER,
90
+ lu_desc VARCHAR,
91
+ limit_type VARCHAR NOT NULL,
92
+ tag VARCHAR,
93
+ status VARCHAR,
94
+ first_lu_time VARCHAR,
95
+ last_lu_time VARCHAR,
96
+ limit_order DOUBLE,
97
+ limit_amount DOUBLE,
98
+ turnover_rate DOUBLE,
99
+ free_float DOUBLE,
100
+ lu_limit_order DOUBLE,
101
+ limit_up_suc_rate DOUBLE,
102
+ turnover DOUBLE,
103
+ sum_float DOUBLE,
104
+ market_type VARCHAR,
105
+ PRIMARY KEY (trade_date, ts_code, limit_type)
106
+ );
107
+
108
+ CREATE TABLE IF NOT EXISTS lub_stage_results (
109
+ run_id UUID NOT NULL,
110
+ stage VARCHAR NOT NULL, -- 'r1' | 'r2' | 'final_ranking'
111
+ batch_no INTEGER,
112
+ trade_date VARCHAR NOT NULL,
113
+ ts_code VARCHAR NOT NULL,
114
+ name VARCHAR,
115
+ score DOUBLE,
116
+ rank INTEGER,
117
+ decision VARCHAR,
118
+ rationale VARCHAR,
119
+ evidence_json VARCHAR,
120
+ risk_flags_json VARCHAR,
121
+ raw_response_json VARCHAR,
122
+ PRIMARY KEY (run_id, stage, ts_code)
123
+ );
124
+
125
+ -- ============================================================
126
+ -- Run history & event stream (formerly framework strategy_runs / strategy_events)
127
+ -- ============================================================
128
+
129
+ CREATE TABLE IF NOT EXISTS lub_runs (
130
+ run_id UUID PRIMARY KEY,
131
+ trade_date VARCHAR NOT NULL,
132
+ status VARCHAR NOT NULL, -- running | success | failed | partial_failed | cancelled
133
+ is_intraday BOOLEAN NOT NULL DEFAULT FALSE,
134
+ started_at TIMESTAMP NOT NULL,
135
+ finished_at TIMESTAMP,
136
+ params_json VARCHAR,
137
+ summary_json VARCHAR,
138
+ error VARCHAR
139
+ );
140
+
141
+ CREATE TABLE IF NOT EXISTS lub_events (
142
+ run_id UUID NOT NULL,
143
+ seq BIGINT NOT NULL,
144
+ event_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
145
+ level VARCHAR NOT NULL,
146
+ event_type VARCHAR NOT NULL,
147
+ message VARCHAR NOT NULL,
148
+ payload_json VARCHAR,
149
+ PRIMARY KEY (run_id, seq)
150
+ );
@@ -0,0 +1,8 @@
1
+ -- v0.8 — debate mode: distinguish per-LLM rows in lub_stage_results.
2
+ -- llm_provider is NULL for non-debate runs and for cross-provider rows;
3
+ -- in debate mode each provider's r1 / r2_initial / r2_revised /
4
+ -- r2_final_initial rows are tagged with the provider name (e.g. 'deepseek').
5
+ ALTER TABLE lub_stage_results ADD COLUMN llm_provider VARCHAR;
6
+
7
+ CREATE INDEX IF NOT EXISTS ix_lub_stage_results_run_provider
8
+ ON lub_stage_results(run_id, llm_provider, stage);
@@ -0,0 +1,36 @@
1
+ -- v0.8 — Phase B1: 龙虎榜接入。
2
+ -- top_list / top_inst 在本插件中升级为 required(账户已具权限)。
3
+ -- candidate 未上榜时 lhb_* 字段为 null(合法事实),不进 data_unavailable。
4
+
5
+ CREATE TABLE IF NOT EXISTS lub_top_list (
6
+ trade_date VARCHAR NOT NULL,
7
+ ts_code VARCHAR NOT NULL,
8
+ name VARCHAR,
9
+ close DOUBLE,
10
+ pct_change DOUBLE,
11
+ turnover_rate DOUBLE,
12
+ amount DOUBLE,
13
+ l_sell DOUBLE,
14
+ l_buy DOUBLE,
15
+ l_amount DOUBLE,
16
+ net_amount DOUBLE,
17
+ net_rate DOUBLE,
18
+ amount_rate DOUBLE,
19
+ float_values DOUBLE,
20
+ reason VARCHAR,
21
+ PRIMARY KEY (trade_date, ts_code)
22
+ );
23
+
24
+ CREATE TABLE IF NOT EXISTS lub_top_inst (
25
+ trade_date VARCHAR NOT NULL,
26
+ ts_code VARCHAR NOT NULL,
27
+ exalter VARCHAR NOT NULL,
28
+ side INTEGER NOT NULL, -- 0 = buy, 1 = sell
29
+ buy DOUBLE,
30
+ buy_rate DOUBLE,
31
+ sell DOUBLE,
32
+ sell_rate DOUBLE,
33
+ net_buy DOUBLE,
34
+ reason VARCHAR,
35
+ PRIMARY KEY (trade_date, ts_code, exalter, side)
36
+ );
@@ -0,0 +1,18 @@
1
+ -- v0.8 — Phase B2: 筹码集中度(cyq_perf)接入。
2
+ -- 账户已具 cyq_perf 权限,按 required 接入;失败 → run terminated。
3
+ -- 单只 candidate 在返回中无记录时该 candidate.missing_data 写入 cyq 字段名。
4
+
5
+ CREATE TABLE IF NOT EXISTS lub_cyq_perf (
6
+ trade_date VARCHAR NOT NULL,
7
+ ts_code VARCHAR NOT NULL,
8
+ his_low DOUBLE,
9
+ his_high DOUBLE,
10
+ cost_5pct DOUBLE,
11
+ cost_15pct DOUBLE,
12
+ cost_50pct DOUBLE,
13
+ cost_85pct DOUBLE,
14
+ cost_95pct DOUBLE,
15
+ weight_avg DOUBLE,
16
+ winner_rate DOUBLE,
17
+ PRIMARY KEY (trade_date, ts_code)
18
+ );
@@ -0,0 +1,46 @@
1
+ -- v0.8 Phase B1 fix — top_list/top_inst PRIMARY KEY needs to include `reason`.
2
+ --
3
+ -- Tushare returns multiple rows per (trade_date, ts_code) when a stock triggers
4
+ -- the LHB for several reasons on the same day (e.g. 日涨幅偏离值达 7% +
5
+ -- 连续三日累计偏离值达 20%). top_inst is the same: the same (date, code,
6
+ -- exalter, side) can appear under several reasons.
7
+ --
8
+ -- 20260508_001 defined PKs that were narrower than the natural uniqueness key,
9
+ -- so the first real materialize hit ConstraintException. Drop & recreate —
10
+ -- these are tushare-derived caches, no business data loss; next run repopulates.
11
+
12
+ DROP TABLE IF EXISTS lub_top_list;
13
+ DROP TABLE IF EXISTS lub_top_inst;
14
+
15
+ CREATE TABLE lub_top_list (
16
+ trade_date VARCHAR NOT NULL,
17
+ ts_code VARCHAR NOT NULL,
18
+ reason VARCHAR NOT NULL,
19
+ name VARCHAR,
20
+ close DOUBLE,
21
+ pct_change DOUBLE,
22
+ turnover_rate DOUBLE,
23
+ amount DOUBLE,
24
+ l_sell DOUBLE,
25
+ l_buy DOUBLE,
26
+ l_amount DOUBLE,
27
+ net_amount DOUBLE,
28
+ net_rate DOUBLE,
29
+ amount_rate DOUBLE,
30
+ float_values DOUBLE,
31
+ PRIMARY KEY (trade_date, ts_code, reason)
32
+ );
33
+
34
+ CREATE TABLE lub_top_inst (
35
+ trade_date VARCHAR NOT NULL,
36
+ ts_code VARCHAR NOT NULL,
37
+ exalter VARCHAR NOT NULL,
38
+ side INTEGER NOT NULL, -- 0 = buy, 1 = sell
39
+ reason VARCHAR NOT NULL,
40
+ buy DOUBLE,
41
+ buy_rate DOUBLE,
42
+ sell DOUBLE,
43
+ sell_rate DOUBLE,
44
+ net_buy DOUBLE,
45
+ PRIMARY KEY (trade_date, ts_code, exalter, side, reason)
46
+ );
@@ -0,0 +1,53 @@
1
+ -- v0.8 Phase B1 fix #2 — drop PRIMARY KEY on lub_top_list / lub_top_inst.
2
+ --
3
+ -- 20260508_003 widened the PK to include `reason`, but Tushare data still has
4
+ -- legitimate duplicates beyond the natural composite key:
5
+ --
6
+ -- * top_inst: anonymous institutional seats are reported as exalter="机构专用"
7
+ -- (or "深股通专用" / "沪股通专用"). A single LHB list has up to 5 buy seats
8
+ -- and 5 sell seats; multiple slots on the same side can all show as
9
+ -- "机构专用" because the actual identity is hidden — Tushare emits each as
10
+ -- a separate row with identical (trade_date, ts_code, exalter, side, reason).
11
+ -- This is legitimate multi-seat semantics, NOT bad data.
12
+ --
13
+ -- * top_list: some upstream-pushed rows arrive duplicated on the natural
14
+ -- key (e.g. 688755.SH 融资类规则). The reason field doesn't always fully
15
+ -- distinguish.
16
+ --
17
+ -- DB-enforced uniqueness was the wrong abstraction for these tables. Drop the
18
+ -- PK; rely on materialize()'s natural-key DELETE → INSERT for idempotency, keep
19
+ -- NOT NULL on the natural-key columns for data quality.
20
+
21
+ DROP TABLE IF EXISTS lub_top_list;
22
+ DROP TABLE IF EXISTS lub_top_inst;
23
+
24
+ CREATE TABLE lub_top_list (
25
+ trade_date VARCHAR NOT NULL,
26
+ ts_code VARCHAR NOT NULL,
27
+ reason VARCHAR NOT NULL,
28
+ name VARCHAR,
29
+ close DOUBLE,
30
+ pct_change DOUBLE,
31
+ turnover_rate DOUBLE,
32
+ amount DOUBLE,
33
+ l_sell DOUBLE,
34
+ l_buy DOUBLE,
35
+ l_amount DOUBLE,
36
+ net_amount DOUBLE,
37
+ net_rate DOUBLE,
38
+ amount_rate DOUBLE,
39
+ float_values DOUBLE
40
+ );
41
+
42
+ CREATE TABLE lub_top_inst (
43
+ trade_date VARCHAR NOT NULL,
44
+ ts_code VARCHAR NOT NULL,
45
+ exalter VARCHAR NOT NULL,
46
+ side INTEGER NOT NULL, -- 0 = buy, 1 = sell
47
+ reason VARCHAR NOT NULL,
48
+ buy DOUBLE,
49
+ buy_rate DOUBLE,
50
+ sell DOUBLE,
51
+ sell_rate DOUBLE,
52
+ net_buy DOUBLE
53
+ );
@@ -0,0 +1,17 @@
1
+ -- v0.4 — plugin-local settings store for limit-up-board.
2
+ --
3
+ -- Holds user-tunable run filters (流通市值 / 当前股价 上限). Distinct from the
4
+ -- framework-level app_config table: framework keys are namespaced by the
5
+ -- whitelisted AppConfig schema; per-plugin tunables live here so the framework
6
+ -- stays free of plugin-specific knobs (Plan A pure isolation).
7
+ --
8
+ -- Keys currently in use (defaults are owned by limit_up_board.config:LubConfig
9
+ -- and re-applied automatically when a row is missing — no DEFAULT here):
10
+ -- lub.max_float_mv_yi — max 流通市值 in 亿
11
+ -- lub.max_close_yuan — max 当前股价 in 元
12
+
13
+ CREATE TABLE IF NOT EXISTS lub_config (
14
+ key VARCHAR PRIMARY KEY,
15
+ value_json VARCHAR NOT NULL,
16
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
17
+ );