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,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
|
+
);
|
|
File without changes
|