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,327 @@
|
|
|
1
|
+
"""LLM pipeline for the volume-anomaly走势分析阶段。
|
|
2
|
+
|
|
3
|
+
Single-stage strategy: no R2 / final_ranking. Multi-batch is supported but the
|
|
4
|
+
batch outputs are simply concatenated (no global re-rank), per the user's spec.
|
|
5
|
+
|
|
6
|
+
Borrows the dual input/output token budget pattern from limit-up-board, but
|
|
7
|
+
does NOT import from it (plugins are self-contained).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from collections.abc import Iterable
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from pydantic import BaseModel
|
|
18
|
+
|
|
19
|
+
from deeptrade.core.llm_client import (
|
|
20
|
+
LLMClient,
|
|
21
|
+
LLMTransportError,
|
|
22
|
+
LLMValidationError,
|
|
23
|
+
)
|
|
24
|
+
from deeptrade.plugins_api import StageProfile
|
|
25
|
+
from deeptrade.plugins_api.events import EventLevel, EventType, StrategyEvent
|
|
26
|
+
|
|
27
|
+
from .data import AnalyzeBundle
|
|
28
|
+
from .profiles import STAGE_TREND_ANALYSIS, resolve_profile
|
|
29
|
+
from .prompts import VA_TREND_SYSTEM, va_trend_user_prompt
|
|
30
|
+
from .schemas import VATrendCandidate, VATrendResponse
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Token budgets / batching
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# Per-candidate cost is higher than limit-up-board's R1 because we ship 60d
|
|
41
|
+
# aggregates + 5d recent OHLCV + moneyflow summary per stock.
|
|
42
|
+
DEFAULT_AVG_INPUT_TOKENS_PER_CANDIDATE = 1_500
|
|
43
|
+
# v0.6.0 — bumped from 900 → 1_100 to absorb dimension_scores (6 ints) +
|
|
44
|
+
# alpha_*_pct fields without crowding output budgets at 25-candidate batches.
|
|
45
|
+
DEFAULT_AVG_OUTPUT_TOKENS_PER_CANDIDATE = 1_100
|
|
46
|
+
DEFAULT_INPUT_BUDGET = 200_000 # matches continuation_prediction stage
|
|
47
|
+
SAFETY_RATIO = 0.85
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class BatchPlan:
|
|
52
|
+
batch_size: int
|
|
53
|
+
n_batches: int
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def plan_batches(
|
|
57
|
+
*,
|
|
58
|
+
n_candidates: int,
|
|
59
|
+
input_budget: int,
|
|
60
|
+
output_budget: int,
|
|
61
|
+
overhead_input_tokens: int = 5_000,
|
|
62
|
+
avg_in: int = DEFAULT_AVG_INPUT_TOKENS_PER_CANDIDATE,
|
|
63
|
+
avg_out: int = DEFAULT_AVG_OUTPUT_TOKENS_PER_CANDIDATE,
|
|
64
|
+
) -> BatchPlan:
|
|
65
|
+
"""Pick the largest batch_size satisfying BOTH input and output budgets."""
|
|
66
|
+
if n_candidates <= 0:
|
|
67
|
+
return BatchPlan(batch_size=0, n_batches=0)
|
|
68
|
+
in_room = max(0, input_budget - overhead_input_tokens)
|
|
69
|
+
by_in = max(1, in_room // max(avg_in, 1))
|
|
70
|
+
by_out = max(1, int(output_budget * SAFETY_RATIO) // max(avg_out, 1))
|
|
71
|
+
batch_size = max(1, min(by_in, by_out, n_candidates))
|
|
72
|
+
n_batches = (n_candidates + batch_size - 1) // batch_size
|
|
73
|
+
return BatchPlan(batch_size=batch_size, n_batches=n_batches)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
# Set equality check (input candidate_id ⊆ ⊇ output)
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _ids(items: Any) -> set[str]:
|
|
82
|
+
return {(c["candidate_id"] if isinstance(c, dict) else c.candidate_id) for c in items}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _set_mismatch_repair_hint(expected: set[str], actual: set[str]) -> str:
|
|
86
|
+
missing = sorted(expected - actual)
|
|
87
|
+
extra = sorted(actual - expected)
|
|
88
|
+
parts = ["\n\n⚠ 上一次响应集合不一致,请严格按照原 candidate_id 列表重新输出。"]
|
|
89
|
+
if missing:
|
|
90
|
+
parts.append(f"missing (你必须包含): {missing}")
|
|
91
|
+
if extra:
|
|
92
|
+
parts.append(f"extra (你不能包含): {extra}")
|
|
93
|
+
parts.append("不可遗漏,不可新增,不可改名。")
|
|
94
|
+
return "\n".join(parts)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class _SetMismatchError(Exception):
|
|
98
|
+
"""LLM output's candidate_id set still differs after repair retry."""
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _complete_with_set_check(
|
|
102
|
+
llm: LLMClient,
|
|
103
|
+
*,
|
|
104
|
+
system: str,
|
|
105
|
+
user: str,
|
|
106
|
+
schema: type[BaseModel],
|
|
107
|
+
profile: StageProfile,
|
|
108
|
+
expected_ids: set[str],
|
|
109
|
+
output_attr: str = "candidates",
|
|
110
|
+
repair_retries: int = 1,
|
|
111
|
+
envelope_defaults: dict[str, Any] | None = None,
|
|
112
|
+
) -> tuple[Any, dict[str, Any]]:
|
|
113
|
+
"""Call LLM once; on candidate_id set mismatch, append a repair hint and retry."""
|
|
114
|
+
current_user = user
|
|
115
|
+
last_actual: set[str] = set()
|
|
116
|
+
for attempt in range(repair_retries + 1):
|
|
117
|
+
raw, meta = llm.complete_json(
|
|
118
|
+
system=system,
|
|
119
|
+
user=current_user,
|
|
120
|
+
schema=schema,
|
|
121
|
+
profile=profile,
|
|
122
|
+
envelope_defaults=envelope_defaults,
|
|
123
|
+
)
|
|
124
|
+
obj = raw if isinstance(raw, schema) else schema.model_validate(raw)
|
|
125
|
+
items = getattr(obj, output_attr)
|
|
126
|
+
actual_ids = {item.candidate_id for item in items}
|
|
127
|
+
if expected_ids == actual_ids:
|
|
128
|
+
return obj, meta
|
|
129
|
+
last_actual = actual_ids
|
|
130
|
+
if attempt < repair_retries:
|
|
131
|
+
current_user = user + _set_mismatch_repair_hint(expected_ids, actual_ids)
|
|
132
|
+
raise _SetMismatchError(
|
|
133
|
+
f"set mismatch after {repair_retries + 1} attempts; "
|
|
134
|
+
f"missing={sorted(expected_ids - last_actual)}, "
|
|
135
|
+
f"extra={sorted(last_actual - expected_ids)}"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
# Pipeline result container
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclass
|
|
145
|
+
class AnalyzeResult:
|
|
146
|
+
"""Outcome of the走势分析 LLM phase."""
|
|
147
|
+
|
|
148
|
+
success_batches: int = 0
|
|
149
|
+
failed_batches: int = 0
|
|
150
|
+
candidates_in: int = 0
|
|
151
|
+
candidates_out: int = 0
|
|
152
|
+
predictions: list[VATrendCandidate] = field(default_factory=list)
|
|
153
|
+
failed_batch_ids: list[str] = field(default_factory=list)
|
|
154
|
+
batch_size: int = 0
|
|
155
|
+
market_context_summaries: list[str] = field(default_factory=list)
|
|
156
|
+
risk_disclaimers: list[str] = field(default_factory=list)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
# Run analyze stage
|
|
161
|
+
# ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def run_analyze(
|
|
165
|
+
*,
|
|
166
|
+
llm: LLMClient,
|
|
167
|
+
bundle: AnalyzeBundle,
|
|
168
|
+
preset: str,
|
|
169
|
+
input_budget: int = DEFAULT_INPUT_BUDGET,
|
|
170
|
+
) -> Iterable[tuple[StrategyEvent, AnalyzeResult | None]]:
|
|
171
|
+
"""Run all analyze batches, yielding (event, terminal_result_or_None).
|
|
172
|
+
|
|
173
|
+
The final iteration emits a STEP_FINISHED event paired with the populated
|
|
174
|
+
AnalyzeResult so the caller can hand it to render.
|
|
175
|
+
"""
|
|
176
|
+
profile = resolve_profile(preset, STAGE_TREND_ANALYSIS)
|
|
177
|
+
candidates = bundle.candidates
|
|
178
|
+
plan = plan_batches(
|
|
179
|
+
n_candidates=len(candidates),
|
|
180
|
+
input_budget=input_budget,
|
|
181
|
+
output_budget=profile.max_output_tokens,
|
|
182
|
+
)
|
|
183
|
+
yield (
|
|
184
|
+
StrategyEvent(
|
|
185
|
+
type=EventType.LIVE_STATUS,
|
|
186
|
+
message=(
|
|
187
|
+
f"[走势分析] 待处理 {len(candidates)} 只,分 {plan.n_batches} 批提交..."
|
|
188
|
+
),
|
|
189
|
+
),
|
|
190
|
+
None,
|
|
191
|
+
)
|
|
192
|
+
yield (
|
|
193
|
+
StrategyEvent(
|
|
194
|
+
type=EventType.STEP_STARTED,
|
|
195
|
+
message="走势分析(主升浪启动预测)",
|
|
196
|
+
payload={"n_candidates": len(candidates), "n_batches": plan.n_batches},
|
|
197
|
+
),
|
|
198
|
+
None,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
result = AnalyzeResult(candidates_in=len(candidates), batch_size=plan.batch_size)
|
|
202
|
+
if plan.n_batches == 0:
|
|
203
|
+
yield (
|
|
204
|
+
StrategyEvent(
|
|
205
|
+
type=EventType.STEP_FINISHED,
|
|
206
|
+
message="走势分析(主升浪启动预测)",
|
|
207
|
+
payload={"predictions": 0},
|
|
208
|
+
),
|
|
209
|
+
result,
|
|
210
|
+
)
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
for i in range(plan.n_batches):
|
|
214
|
+
batch = candidates[i * plan.batch_size : (i + 1) * plan.batch_size]
|
|
215
|
+
yield (
|
|
216
|
+
StrategyEvent(
|
|
217
|
+
type=EventType.LIVE_STATUS,
|
|
218
|
+
message=(
|
|
219
|
+
f"[走势分析] 已提交第 {i + 1}/{plan.n_batches} 批 "
|
|
220
|
+
f"({len(batch)} 只),等待 LLM 响应..."
|
|
221
|
+
),
|
|
222
|
+
),
|
|
223
|
+
None,
|
|
224
|
+
)
|
|
225
|
+
yield (
|
|
226
|
+
StrategyEvent(
|
|
227
|
+
type=EventType.LLM_BATCH_STARTED,
|
|
228
|
+
message=f"analyze batch {i + 1}/{plan.n_batches}",
|
|
229
|
+
payload={"batch_no": i + 1, "size": len(batch)},
|
|
230
|
+
),
|
|
231
|
+
None,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
user = va_trend_user_prompt(
|
|
235
|
+
trade_date=bundle.trade_date,
|
|
236
|
+
next_trade_date=bundle.next_trade_date,
|
|
237
|
+
batch_no=i + 1,
|
|
238
|
+
batch_total=plan.n_batches,
|
|
239
|
+
candidates=batch,
|
|
240
|
+
market_summary=bundle.market_summary,
|
|
241
|
+
sector_strength_source=bundle.sector_strength_source,
|
|
242
|
+
sector_strength_data=bundle.sector_strength_data,
|
|
243
|
+
data_unavailable=bundle.data_unavailable,
|
|
244
|
+
)
|
|
245
|
+
expected_ids = _ids(batch)
|
|
246
|
+
try:
|
|
247
|
+
obj, meta = _complete_with_set_check(
|
|
248
|
+
llm,
|
|
249
|
+
system=VA_TREND_SYSTEM,
|
|
250
|
+
user=user,
|
|
251
|
+
schema=VATrendResponse,
|
|
252
|
+
profile=profile,
|
|
253
|
+
expected_ids=expected_ids,
|
|
254
|
+
envelope_defaults={
|
|
255
|
+
"stage": STAGE_TREND_ANALYSIS,
|
|
256
|
+
"trade_date": bundle.trade_date,
|
|
257
|
+
"next_trade_date": bundle.next_trade_date,
|
|
258
|
+
"batch_no": i + 1,
|
|
259
|
+
"batch_total": plan.n_batches,
|
|
260
|
+
"market_context_summary": "",
|
|
261
|
+
"risk_disclaimer": "",
|
|
262
|
+
},
|
|
263
|
+
)
|
|
264
|
+
except (LLMValidationError, LLMTransportError, _SetMismatchError) as e:
|
|
265
|
+
result.failed_batches += 1
|
|
266
|
+
result.failed_batch_ids.append(f"analyze.batch.{i + 1}")
|
|
267
|
+
yield (
|
|
268
|
+
StrategyEvent(
|
|
269
|
+
type=EventType.VALIDATION_FAILED,
|
|
270
|
+
level=EventLevel.ERROR,
|
|
271
|
+
message=f"analyze batch {i + 1} failed: {e}",
|
|
272
|
+
payload={"batch_no": i + 1},
|
|
273
|
+
),
|
|
274
|
+
None,
|
|
275
|
+
)
|
|
276
|
+
continue
|
|
277
|
+
|
|
278
|
+
result.success_batches += 1
|
|
279
|
+
result.candidates_out += len(obj.candidates)
|
|
280
|
+
result.predictions.extend(obj.candidates)
|
|
281
|
+
result.market_context_summaries.append(obj.market_context_summary)
|
|
282
|
+
result.risk_disclaimers.append(obj.risk_disclaimer)
|
|
283
|
+
yield (
|
|
284
|
+
StrategyEvent(
|
|
285
|
+
type=EventType.LLM_BATCH_FINISHED,
|
|
286
|
+
message=f"analyze batch {i + 1}/{plan.n_batches} ok",
|
|
287
|
+
payload={
|
|
288
|
+
"batch_no": i + 1,
|
|
289
|
+
"input_tokens": meta["input_tokens"],
|
|
290
|
+
"output_tokens": meta["output_tokens"],
|
|
291
|
+
},
|
|
292
|
+
),
|
|
293
|
+
None,
|
|
294
|
+
)
|
|
295
|
+
n_imminent = sum(1 for c in result.predictions if c.prediction == "imminent_launch")
|
|
296
|
+
yield (
|
|
297
|
+
StrategyEvent(
|
|
298
|
+
type=EventType.LIVE_STATUS,
|
|
299
|
+
message=(
|
|
300
|
+
f"[走势分析] 第 {i + 1}/{plan.n_batches} 批响应已收到 "
|
|
301
|
+
f"(累计 imminent_launch {n_imminent} 只)"
|
|
302
|
+
),
|
|
303
|
+
),
|
|
304
|
+
None,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
yield (
|
|
308
|
+
StrategyEvent(
|
|
309
|
+
type=EventType.LIVE_STATUS,
|
|
310
|
+
message=(
|
|
311
|
+
f"[走势分析] 完成 — 共 {len(result.predictions)}/{result.candidates_in} 个预测"
|
|
312
|
+
),
|
|
313
|
+
),
|
|
314
|
+
None,
|
|
315
|
+
)
|
|
316
|
+
yield (
|
|
317
|
+
StrategyEvent(
|
|
318
|
+
type=EventType.STEP_FINISHED,
|
|
319
|
+
message="走势分析(主升浪启动预测)",
|
|
320
|
+
payload={
|
|
321
|
+
"success_batches": result.success_batches,
|
|
322
|
+
"failed_batches": result.failed_batches,
|
|
323
|
+
"predictions": len(result.predictions),
|
|
324
|
+
},
|
|
325
|
+
),
|
|
326
|
+
result,
|
|
327
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""VolumeAnomalyPlugin — Plugin Protocol entry for the成交量异动策略."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from . import cli as _cli
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
10
|
+
from deeptrade.plugins_api.base import PluginContext
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class VolumeAnomalyPlugin:
|
|
14
|
+
"""Framework entry class for the volume-anomaly plugin."""
|
|
15
|
+
|
|
16
|
+
metadata = None # injected by framework after install
|
|
17
|
+
|
|
18
|
+
def validate_static(self, ctx: PluginContext) -> None: # noqa: ARG002
|
|
19
|
+
from . import schemas # noqa: F401, PLC0415
|
|
20
|
+
|
|
21
|
+
def dispatch(self, argv: list[str]) -> int:
|
|
22
|
+
return _cli.main(argv)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""LLM stage profile presets for volume-anomaly.
|
|
2
|
+
|
|
3
|
+
v0.7 — stage 调参档归插件维护。本插件单 stage(``trend_analysis``),名称
|
|
4
|
+
自然回归语义对应位(之前因为框架硬编码 stage 表,借用了 limit-up-board 的
|
|
5
|
+
``continuation_prediction`` 名)。
|
|
6
|
+
|
|
7
|
+
Preset 语义沿用 v0.6 ``PROFILES_DEFAULT`` 中 R2 阶段对应档(balanced/quality
|
|
8
|
+
开 thinking,fast 关)。
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from deeptrade.plugins_api import StageProfile
|
|
14
|
+
|
|
15
|
+
STAGE_TREND_ANALYSIS = "trend_analysis"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
PROFILES: dict[str, dict[str, StageProfile]] = {
|
|
19
|
+
"fast": {
|
|
20
|
+
STAGE_TREND_ANALYSIS: StageProfile(
|
|
21
|
+
thinking=False, reasoning_effort="medium", temperature=0.2, max_output_tokens=32768
|
|
22
|
+
),
|
|
23
|
+
},
|
|
24
|
+
"balanced": {
|
|
25
|
+
STAGE_TREND_ANALYSIS: StageProfile(
|
|
26
|
+
thinking=True, reasoning_effort="high", temperature=0.2, max_output_tokens=32768
|
|
27
|
+
),
|
|
28
|
+
},
|
|
29
|
+
"quality": {
|
|
30
|
+
STAGE_TREND_ANALYSIS: StageProfile(
|
|
31
|
+
thinking=True, reasoning_effort="high", temperature=0.2, max_output_tokens=32768
|
|
32
|
+
),
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def resolve_profile(preset: str, stage: str) -> StageProfile:
|
|
38
|
+
"""Look up the StageProfile for ``preset × stage``."""
|
|
39
|
+
if preset not in PROFILES:
|
|
40
|
+
raise KeyError(
|
|
41
|
+
f"unknown profile preset {preset!r}; expected one of {sorted(PROFILES)}"
|
|
42
|
+
)
|
|
43
|
+
table = PROFILES[preset]
|
|
44
|
+
if stage not in table:
|
|
45
|
+
raise KeyError(
|
|
46
|
+
f"unknown stage {stage!r} for preset {preset!r}; "
|
|
47
|
+
f"expected one of {sorted(table)}"
|
|
48
|
+
)
|
|
49
|
+
return table[stage]
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Prompt templates for the volume-anomaly走势分析阶段。
|
|
2
|
+
|
|
3
|
+
Single-stage strategy (no R1/R2/final_ranking). Per the user's spec:
|
|
4
|
+
* 删除「异动确认」维度(已在筛选环节硬性满足,无需 LLM 重复判断)
|
|
5
|
+
* 增加「是否经过充分洗盘」维度(洗盘越充分启动概率越大)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from .prompts_examples import VA_TREND_FEWSHOT
|
|
14
|
+
|
|
15
|
+
_VA_TREND_SYSTEM_BASE = """\
|
|
16
|
+
你是 A 股「主升浪启动预测」研究助手。你只能基于本次消息中提供的结构化数据进行分析。
|
|
17
|
+
|
|
18
|
+
【硬性纪律】
|
|
19
|
+
1. 严禁使用外部搜索、新闻网站、公告网站、实时行情、社交媒体、机构观点或任何未提供的数据。
|
|
20
|
+
2. 严禁编造新闻、公告、盘口、传闻、龙虎榜席位(除非数据中明确提供)、资金分歧、ETF 申赎流向。
|
|
21
|
+
3. 如果某字段缺失(出现在 data_unavailable 或单只 missing_data 中),必须显式声明,禁止猜测。
|
|
22
|
+
4. 输入清单中的每一只标的都必须出现在 candidates 数组,candidate_id 与输入完全一致,不可漏不可加。
|
|
23
|
+
5. 仅输出 JSON,不要 Markdown 代码块包裹,不要解释性前后缀。
|
|
24
|
+
|
|
25
|
+
【任务】
|
|
26
|
+
对本批次"已通过本地异动筛选并入池追踪"的标的进行『主升浪启动预测』。判断该标的在 1-3 个交易日内启动主升浪的概率。
|
|
27
|
+
|
|
28
|
+
【判断维度】
|
|
29
|
+
A. **是否经过充分洗盘** —— 启动前的洗盘越充分,启动概率越大;具体观察:
|
|
30
|
+
- 异动前的整理周期长度(base_days);越长越好
|
|
31
|
+
- 整理期间的最大回撤(base_max_drawdown_pct);幅度越深、量越缩越好
|
|
32
|
+
- 整理期间的成交量是否持续萎缩(base_vol_shrink_ratio)
|
|
33
|
+
- 整理期间换手率是否充分(base_avg_turnover_rate)
|
|
34
|
+
- 异动前距上一次涨停的天数(days_since_last_limit_up,若有)
|
|
35
|
+
- 整理期间的波动率是否收敛(atr_10d_quantile_in_60d / bbw_compression_ratio);
|
|
36
|
+
**越低越好**——三维齐降(价收敛 + 量缩 + 波动率收敛)是 VCP 的教科书形态
|
|
37
|
+
→ 对应 `dimension_scores.washout`
|
|
38
|
+
washout_quality ∈ {sufficient, partial, insufficient, unclear}
|
|
39
|
+
B. **形态结构**:与 ma5/ma10/ma20/ma60 的关系;是否平台/底部突破;是否上升趋势右侧
|
|
40
|
+
→ 对应 `dimension_scores.pattern`
|
|
41
|
+
C. **资金验证**:近 5 日 moneyflow 大单/特大单净流入趋势;net_mf 是否 5 日累计为正
|
|
42
|
+
→ 对应 `dimension_scores.capital`
|
|
43
|
+
D. **板块与市场相对强度**:
|
|
44
|
+
- 板块层:参考输入【板块强度摘要】sector_strength_source(可信度 limit_cpt_list > 行业聚合)
|
|
45
|
+
- 市场层:用 `alpha_5d_pct` / `alpha_20d_pct` / `alpha_60d_pct`(个股相对沪深 300)+
|
|
46
|
+
`rel_strength_label ∈ {leading, in_line, lagging}`(基于 alpha_20d_pct 分档)判断
|
|
47
|
+
抗大盘强度。**baseline 下跌时个股仍上涨 → 抗跌强势(强信号)**;
|
|
48
|
+
baseline 上涨而个股跟随 → 弱跟随。alpha 字段可能因数据降级为 None,
|
|
49
|
+
必须在 missing_data 中显式声明。
|
|
50
|
+
→ 对应 `dimension_scores.sector`
|
|
51
|
+
E. **历史强度**:近 60 日是否已有过涨停 / 是否已经处于二浪 / 距首次异动天数(tracked_days)
|
|
52
|
+
→ 对应 `dimension_scores.historical`
|
|
53
|
+
F. **风险**:是否高位放量出货 / 流通盘过大 / 题材孤立 / 超买连阳 / 缺数据
|
|
54
|
+
→ 对应 `dimension_scores.risk`(**反向打分:分越高代表风险越大**)
|
|
55
|
+
|
|
56
|
+
【evidence 要求】
|
|
57
|
+
每只 1-5 条 key_evidence;每条必须引用真实出现在输入中的字段名 (`field`),并填上对应数值 (`value`)、单位 (`unit`) 和你的解读 (`interpretation`)。
|
|
58
|
+
任何无法用输入字段佐证的 rationale 都视为幻觉。
|
|
59
|
+
rationale 不超过 200 字。
|
|
60
|
+
|
|
61
|
+
【dimension_scores 评分尺度】
|
|
62
|
+
对每个维度(washout / pattern / capital / sector / historical / risk)输出一个 0–100 的整数评分:
|
|
63
|
+
- 0–30:明显不利 / 不充分
|
|
64
|
+
- 30–60:中性 / 部分满足
|
|
65
|
+
- 60–80:明显有利 / 较充分
|
|
66
|
+
- 80–100:教科书级 / 极充分(保留给罕见的极端正例 / 极端风险)
|
|
67
|
+
|
|
68
|
+
**风险维度方向相反**——分越高代表风险越大;其余维度都是正向评分。
|
|
69
|
+
launch_score 应大致反映各维度综合,但不强制公式约束(保留模型自洽空间)。
|
|
70
|
+
|
|
71
|
+
【输出语义】
|
|
72
|
+
- launch_score (0-100): 主升浪启动概率分(模型内部排序分)
|
|
73
|
+
- prediction:
|
|
74
|
+
* imminent_launch — 1-3 个交易日内启动概率高(强信号 + 充分洗盘 + 板块支撑)
|
|
75
|
+
* watching — 形态正在构筑,需要再观察
|
|
76
|
+
* not_yet — 时机未到 / 已在末段 / 数据矛盾
|
|
77
|
+
- pattern:
|
|
78
|
+
* breakout — 突破整理平台
|
|
79
|
+
* consolidation_break — 缩量整理后温和放量启动
|
|
80
|
+
* first_wave — 一浪初动(早期)
|
|
81
|
+
* second_leg — 二浪起涨
|
|
82
|
+
* unclear — 形态不清晰
|
|
83
|
+
- washout_quality:
|
|
84
|
+
* sufficient — 整理周期长、回撤充分、缩量明显、换手到位
|
|
85
|
+
* partial — 仅满足部分维度
|
|
86
|
+
* insufficient — 几乎没有洗盘(追高风险)
|
|
87
|
+
* unclear — 数据不足
|
|
88
|
+
|
|
89
|
+
【输出格式】(严格按照此 JSON Schema 输出;不要省略任何字段,不要新增字段)
|
|
90
|
+
{
|
|
91
|
+
"stage": "continuation_prediction",
|
|
92
|
+
"trade_date": "<原样回传输入中的 trade_date>",
|
|
93
|
+
"next_trade_date": "<原样回传输入中的 next_trade_date>",
|
|
94
|
+
"batch_no": <原样回传输入中的 batch_no>,
|
|
95
|
+
"batch_total": <原样回传输入中的 batch_total>,
|
|
96
|
+
"market_context_summary": "<整体市场背景 ≤ 100 字>",
|
|
97
|
+
"risk_disclaimer": "<风险提示 ≤ 80 字>",
|
|
98
|
+
"candidates": [
|
|
99
|
+
{
|
|
100
|
+
"candidate_id": "<原样回传输入中的 candidate_id>",
|
|
101
|
+
"ts_code": "<原样回传,含 .SH/.SZ 后缀>",
|
|
102
|
+
"name": "<原样回传输入中的股票名称>",
|
|
103
|
+
"rank": 1,
|
|
104
|
+
"launch_score": 0,
|
|
105
|
+
"confidence": "high",
|
|
106
|
+
"prediction": "imminent_launch",
|
|
107
|
+
"pattern": "breakout",
|
|
108
|
+
"washout_quality": "sufficient",
|
|
109
|
+
"rationale": "<≤ 200 字的核心判断>",
|
|
110
|
+
"dimension_scores": {
|
|
111
|
+
"washout": 60,
|
|
112
|
+
"pattern": 60,
|
|
113
|
+
"capital": 60,
|
|
114
|
+
"sector": 60,
|
|
115
|
+
"historical": 60,
|
|
116
|
+
"risk": 30
|
|
117
|
+
},
|
|
118
|
+
"key_evidence": [
|
|
119
|
+
{
|
|
120
|
+
"field": "<必须是输入字段名,如 base_days / vol_ratio_5d / ma60>",
|
|
121
|
+
"value": 0,
|
|
122
|
+
"unit": "<亿/万/%/日/次/无>",
|
|
123
|
+
"interpretation": "<对该数值的简短解读>"
|
|
124
|
+
}
|
|
125
|
+
],
|
|
126
|
+
"next_session_watch": ["<次日需要观察的 1-4 个关键点>"],
|
|
127
|
+
"invalidation_triggers": ["<会让预测失效的 1-4 个触发条件>"],
|
|
128
|
+
"risk_flags": [],
|
|
129
|
+
"missing_data": []
|
|
130
|
+
}
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
【字段值约束】
|
|
135
|
+
- rank: 本批内 1..N 连续唯一整数(不可重复、不可跳号)
|
|
136
|
+
- launch_score: 0–100 浮点数
|
|
137
|
+
- confidence: "high" / "medium" / "low" 三选一
|
|
138
|
+
- prediction: "imminent_launch" / "watching" / "not_yet" 三选一
|
|
139
|
+
- pattern: "breakout" / "consolidation_break" / "first_wave" / "second_leg" / "unclear" 五选一
|
|
140
|
+
- washout_quality: "sufficient" / "partial" / "insufficient" / "unclear" 四选一
|
|
141
|
+
- dimension_scores: 6 个维度(washout/pattern/capital/sector/historical/risk)每个 0–100 整数;不可省
|
|
142
|
+
- key_evidence: 每只 1–5 条,每条 4 个字段不可省
|
|
143
|
+
- next_session_watch / invalidation_triggers: 各 1–4 条字符串数组(不可为空)
|
|
144
|
+
- risk_flags: 空数组或字符串数组,最多 5 条
|
|
145
|
+
- missing_data: 数据缺失字段名数组(参见 data_unavailable)
|
|
146
|
+
- 输入清单中每一只标的必须出现在 candidates 中,candidate_id 与输入完全一致。
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# v0.3.0 P0-5 — concatenate the few-shot examples block onto the system prompt
|
|
151
|
+
# so all batches see the same anchoring scale across LLM providers / model
|
|
152
|
+
# upgrades. The few-shot adds ~800–1000 tokens; well within the 200K budget.
|
|
153
|
+
VA_TREND_SYSTEM = _VA_TREND_SYSTEM_BASE + VA_TREND_FEWSHOT
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def va_trend_user_prompt(
|
|
157
|
+
*,
|
|
158
|
+
trade_date: str,
|
|
159
|
+
next_trade_date: str,
|
|
160
|
+
batch_no: int,
|
|
161
|
+
batch_total: int,
|
|
162
|
+
candidates: list[dict[str, Any]],
|
|
163
|
+
market_summary: dict[str, Any],
|
|
164
|
+
sector_strength_source: str,
|
|
165
|
+
sector_strength_data: dict[str, Any],
|
|
166
|
+
data_unavailable: list[str],
|
|
167
|
+
) -> str:
|
|
168
|
+
"""Render the走势分析 user prompt for one batch."""
|
|
169
|
+
return (
|
|
170
|
+
f"trade_date = {trade_date}\n"
|
|
171
|
+
f"next_trade_date= {next_trade_date}\n"
|
|
172
|
+
f"batch_no = {batch_no}\n"
|
|
173
|
+
f"batch_total = {batch_total}\n"
|
|
174
|
+
f"本批候选股 = {len(candidates)} 只\n"
|
|
175
|
+
f"全局 data_unavailable = {data_unavailable}\n\n"
|
|
176
|
+
"【市场摘要】\n"
|
|
177
|
+
+ json.dumps(market_summary, ensure_ascii=False, indent=2)
|
|
178
|
+
+ "\n\n【板块强度摘要】\n"
|
|
179
|
+
f"sector_strength_source = {sector_strength_source}\n"
|
|
180
|
+
"sector_strength_data = "
|
|
181
|
+
+ json.dumps(sector_strength_data, ensure_ascii=False, indent=2)
|
|
182
|
+
+ "\n\n【候选清单】(每只含异动入池后的追踪历史聚合 + 整理期/洗盘指标 + 资金摘要)\n"
|
|
183
|
+
+ json.dumps(candidates, ensure_ascii=False, indent=2)
|
|
184
|
+
+ "\n\n请对本批次每一只候选股输出 VATrendCandidate;"
|
|
185
|
+
"candidate_id 与输入一一对应;rank 在本批内唯一且 1..N 连续;"
|
|
186
|
+
"rationale ≤ 200 字。\n"
|
|
187
|
+
)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Few-shot anchoring examples for the volume-anomaly trend-prediction stage.
|
|
2
|
+
|
|
3
|
+
Two synthetic candidates are wired into ``VA_TREND_SYSTEM`` (see prompts.py)
|
|
4
|
+
to anchor LLMs onto a consistent ``launch_score`` scale and to demonstrate the
|
|
5
|
+
expected ``key_evidence`` field-citation discipline.
|
|
6
|
+
|
|
7
|
+
CRITICAL CONSTRAINT: every ``"field": "<X>"`` referenced below MUST appear in
|
|
8
|
+
either the ``hits`` rows produced by ``screen_anomalies`` or the candidate
|
|
9
|
+
rows produced by ``_build_candidate_row``. The
|
|
10
|
+
``test_prompt_consistency.py`` test enforces this — any field rename in
|
|
11
|
+
``data.py`` will turn that test red, preventing silent prompt-schema drift.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
VA_TREND_FEWSHOT = """\
|
|
17
|
+
|
|
18
|
+
【参考示例】(仅展示判断尺度与字段引用规范,不是输入的一部分)
|
|
19
|
+
|
|
20
|
+
示例 A — 教科书式 VCP 缩量充分洗盘后放量突破
|
|
21
|
+
{
|
|
22
|
+
"candidate_id": "000XXX.SZ",
|
|
23
|
+
"ts_code": "000XXX.SZ",
|
|
24
|
+
"name": "示例 A",
|
|
25
|
+
"rank": 1,
|
|
26
|
+
"launch_score": 78,
|
|
27
|
+
"confidence": "high",
|
|
28
|
+
"prediction": "imminent_launch",
|
|
29
|
+
"pattern": "breakout",
|
|
30
|
+
"washout_quality": "sufficient",
|
|
31
|
+
"rationale": "整理 24 日,回撤 12%,波动率分位 0.08;T 日放量站上 MA20;moneyflow 5 日累计净流入;板块为当日主线。三维 VCP 齐降 + 主线共振 → 启动概率高。",
|
|
32
|
+
"dimension_scores": {
|
|
33
|
+
"washout": 80,
|
|
34
|
+
"pattern": 75,
|
|
35
|
+
"capital": 70,
|
|
36
|
+
"sector": 75,
|
|
37
|
+
"historical": 60,
|
|
38
|
+
"risk": 25
|
|
39
|
+
},
|
|
40
|
+
"key_evidence": [
|
|
41
|
+
{"field": "base_days", "value": 24, "unit": "日", "interpretation": "整理周期较长,洗盘相对充分"},
|
|
42
|
+
{"field": "atr_10d_quantile_in_60d", "value": 0.08, "unit": "无", "interpretation": "波动率处于近 60 日 8% 分位,VCP 收敛"},
|
|
43
|
+
{"field": "anomaly_vol_ratio_5d", "value": 2.4, "unit": "倍", "interpretation": "异动当日相对前 5 日放量 2.4 倍"},
|
|
44
|
+
{"field": "dist_to_250d_high_pct", "value": -3.5, "unit": "%", "interpretation": "距 250 日新高仅 3.5%,临近年线突破口"},
|
|
45
|
+
{"field": "alpha_20d_pct", "value": 8.2, "unit": "%", "interpretation": "近 20 日相对沪深 300 跑赢 8.2%,主线领涨"}
|
|
46
|
+
],
|
|
47
|
+
"next_session_watch": ["次日开盘是否站稳 MA10", "板块强度是否延续主线地位"],
|
|
48
|
+
"invalidation_triggers": ["收盘跌破 MA10 且 moneyflow 转为净流出", "板块跌出主线前 5"],
|
|
49
|
+
"risk_flags": [],
|
|
50
|
+
"missing_data": []
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
示例 B — 高位长上影线 + 资金外流
|
|
54
|
+
{
|
|
55
|
+
"candidate_id": "600YYY.SH",
|
|
56
|
+
"ts_code": "600YYY.SH",
|
|
57
|
+
"name": "示例 B",
|
|
58
|
+
"rank": 2,
|
|
59
|
+
"launch_score": 22,
|
|
60
|
+
"confidence": "medium",
|
|
61
|
+
"prediction": "not_yet",
|
|
62
|
+
"pattern": "unclear",
|
|
63
|
+
"washout_quality": "insufficient",
|
|
64
|
+
"rationale": "异动当天上影线占振幅 0.42,body_ratio 仅 0.55;过去 60 日已两次涨停且距 120 日新高 < 1%;moneyflow 5 日累计净流出。高位放量诱多概率高。",
|
|
65
|
+
"dimension_scores": {
|
|
66
|
+
"washout": 30,
|
|
67
|
+
"pattern": 35,
|
|
68
|
+
"capital": 25,
|
|
69
|
+
"sector": 50,
|
|
70
|
+
"historical": 30,
|
|
71
|
+
"risk": 75
|
|
72
|
+
},
|
|
73
|
+
"key_evidence": [
|
|
74
|
+
{"field": "upper_shadow_ratio", "value": 0.42, "unit": "无", "interpretation": "上影线偏长,疑似冲高回落"},
|
|
75
|
+
{"field": "prior_limit_up_count_60d", "value": 2, "unit": "次", "interpretation": "近 60 日已 2 次涨停,浪型偏后"},
|
|
76
|
+
{"field": "dist_to_120d_high_pct", "value": -0.8, "unit": "%", "interpretation": "贴近 120 日新高,套牢盘压力大"},
|
|
77
|
+
{"field": "alpha_20d_pct", "value": -3.0, "unit": "%", "interpretation": "近 20 日相对沪深 300 跑输,板块支撑弱"}
|
|
78
|
+
],
|
|
79
|
+
"next_session_watch": ["放量回踩 MA10 是否守住", "moneyflow 是否转为净流入"],
|
|
80
|
+
"invalidation_triggers": ["再放量阴线击穿 MA20"],
|
|
81
|
+
"risk_flags": ["high_bull_trap_risk", "late_stage_pattern"],
|
|
82
|
+
"missing_data": []
|
|
83
|
+
}
|
|
84
|
+
"""
|