Skip to content

Commit 707c36e

Browse files
committed
fix(review-feedback-1723): 收敛
1 parent 1664b7c commit 707c36e

15 files changed

Lines changed: 305 additions & 48 deletions

api/v1/endpoints/analysis.py

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,14 @@
7070
extract_analysis_context_pack_overview,
7171
sanitize_context_snapshot_for_api,
7272
)
73-
from src.market_phase_summary import extract_market_phase_summary
74-
from src.core.trading_calendar import get_market_for_stock
73+
from src.market_phase_summary import (
74+
extract_market_phase_summary,
75+
rebuild_market_phase_summary_for_stock_code,
76+
)
77+
from src.services.stock_code_utils import is_code_like, resolve_index_stock_code_for_analysis
7578
from src.report_language import get_localized_stock_name, normalize_report_language
7679
from src.schemas.decision_action import build_action_fields
7780
from src.services.name_to_code_resolver import resolve_name_to_code
78-
from src.services.stock_code_utils import is_code_like
7981
from src.services.task_queue import (
8082
get_task_queue,
8183
DuplicateTaskError,
@@ -200,12 +202,8 @@ def _resolve_and_normalize_input(raw_value: str) -> str:
200202
if not text:
201203
return ""
202204

203-
indexed_code = resolve_index_stock_code(text)
204-
if indexed_code:
205-
return canonical_stock_code(indexed_code)
206-
207205
if is_code_like(text):
208-
return canonical_stock_code(text)
206+
return resolve_index_stock_code_for_analysis(text)
209207

210208
if _is_obviously_invalid_analysis_input(text):
211209
raise _invalid_analysis_input_error()
@@ -832,18 +830,10 @@ def _display_stock_code_from_index(stock_code: Any) -> str:
832830

833831

834832
def _display_market_phase_summary(stock_code: Any, context_snapshot: Any) -> Any:
835-
summary = extract_market_phase_summary(context_snapshot)
836-
display_code = _display_stock_code_from_index(stock_code)
837-
if not display_code or display_code == str(stock_code or "").strip():
838-
return summary
839-
840-
market = get_market_for_stock(display_code)
841-
if market not in {"jp", "kr"}:
842-
return summary
843-
844-
if not isinstance(summary, dict):
845-
return summary
846-
return {**summary, "market": market}
833+
return rebuild_market_phase_summary_for_stock_code(
834+
_display_stock_code_from_index(stock_code),
835+
context_snapshot,
836+
)
847837

848838

849839
def _prepare_report_for_task_enrichment(

bot/commands/analyze.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
from bot.commands.base import BotCommand
1515
from bot.models import BotMessage, BotResponse
16-
from data_provider.base import canonical_stock_code
16+
from src.services.stock_code_utils import resolve_index_stock_code_for_analysis
1717

1818
logger = logging.getLogger(__name__)
1919

@@ -67,7 +67,7 @@ def validate_args(self, args: List[str]) -> Optional[str]:
6767

6868
def execute(self, message: BotMessage, args: List[str]) -> BotResponse:
6969
"""执行分析命令"""
70-
code = canonical_stock_code(args[0])
70+
code = resolve_index_stock_code_for_analysis(args[0])
7171

7272
# 检查是否需要完整报告(默认精简,传 full/完整/详细 切换)
7373
report_type = "simple"

main.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,10 @@
6464
import uuid
6565
from datetime import date, datetime, timezone, timedelta
6666

67-
from data_provider.base import canonical_stock_code
6867
from src.webui_frontend import prepare_webui_frontend_assets
6968
from src.config import get_config, Config
7069
from src.logging_config import setup_logging
70+
from src.services.stock_code_utils import resolve_index_stock_code_for_analysis
7171

7272

7373
logger = logging.getLogger(__name__)
@@ -1144,7 +1144,11 @@ def main() -> int:
11441144
# 解析股票列表(统一为大写 Issue #355)
11451145
stock_codes = None
11461146
if args.stocks:
1147-
stock_codes = [canonical_stock_code(c) for c in args.stocks.split(',') if (c or "").strip()]
1147+
stock_codes = [
1148+
resolve_index_stock_code_for_analysis(c)
1149+
for c in args.stocks.split(',')
1150+
if (c or "").strip()
1151+
]
11481152
logger.info(f"使用命令行指定的股票列表: {stock_codes}")
11491153

11501154
# === 处理 --webui / --webui-only 参数,映射到 --serve / --serve-only ===

src/data/stock_index_loader.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,8 @@ def _build_stock_code_lookup(raw_items: list) -> Dict[str, str]:
138138
continue
139139
if not _is_jp_kr_index_code(canonical_code):
140140
continue
141+
if len(item) > 8 and item[8] is False:
142+
continue
141143

142144
_add_code_lookup(exact_lookup, canonical_code, canonical_code)
143145
_add_code_lookup(exact_lookup, display_code, canonical_code)

src/market_phase_summary.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44
from __future__ import annotations
55

66
import json
7+
from datetime import datetime
78
from collections.abc import Mapping
89
from typing import Any, Dict, List, Optional
910

10-
from src.core.trading_calendar import MarketPhase
11+
from src.core.trading_calendar import MarketPhase, build_market_phase_context, get_market_for_stock
1112

1213

1314
MARKET_PHASE_SUMMARY_KEY = "market_phase_summary"
@@ -111,6 +112,56 @@ def extract_market_phase_summary(context_snapshot: Any) -> Optional[Dict[str, An
111112
return render_market_phase_summary(summary)
112113

113114

115+
def _parse_phase_local_time(value: Any) -> Optional[datetime]:
116+
if isinstance(value, datetime):
117+
return value
118+
if isinstance(value, str):
119+
try:
120+
return datetime.fromisoformat(value)
121+
except ValueError:
122+
return None
123+
return None
124+
125+
126+
def rebuild_market_phase_summary_for_stock_code(
127+
stock_code: Any,
128+
context_snapshot: Any,
129+
) -> Optional[Dict[str, Any]]:
130+
"""Rebuild phase summary with derived fields for JP/KR display codes.
131+
132+
Legacy CN snapshots on JP/KR stock records can retain CN-local values. This
133+
helper recomputes those derived fields using the target market context while
134+
preserving non-derived source fields when possible.
135+
"""
136+
summary = extract_market_phase_summary(context_snapshot)
137+
if not isinstance(summary, Mapping):
138+
return None
139+
140+
market = get_market_for_stock(str(stock_code or "").strip())
141+
if market not in {"jp", "kr"}:
142+
return dict(summary)
143+
144+
phase = str(summary.get("phase", "")).strip()
145+
analysis_phase = phase if phase in _ALLOWED_PHASES else "auto"
146+
analysis_intent = str(summary.get("analysis_intent") or "auto").strip()
147+
if not analysis_intent:
148+
analysis_intent = "auto"
149+
150+
rebuilt = build_market_phase_context(
151+
market=market,
152+
current_time=_parse_phase_local_time(summary.get("market_local_time")),
153+
trigger_source=str(summary.get("trigger_source") or "system").strip() or "system",
154+
analysis_intent=analysis_intent,
155+
analysis_phase=analysis_phase,
156+
).to_dict()
157+
158+
rebuilt.setdefault("warnings", list(summary.get("warnings") or []))
159+
if not rebuilt.get("warnings"):
160+
rebuilt["warnings"] = list(summary.get("warnings") or [])
161+
162+
return rebuilt
163+
164+
114165
def normalize_analysis_phase_bucket(value: Any) -> str:
115166
"""Fold detailed phase labels into the public backtest/statistics buckets."""
116167
phase = _safe_text(value)

src/services/history_service.py

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
from typing import Optional, Dict, Any, List, Tuple, TYPE_CHECKING
1717

1818
from src.config import get_config, resolve_news_window_days
19-
from src.core.trading_calendar import get_market_for_stock
2019
from src.data.stock_index_loader import resolve_index_stock_code
2120
from src.report_language import (
2221
get_bias_status_emoji,
@@ -33,7 +32,10 @@
3332
)
3433
from src.storage import DatabaseManager
3534
from src.services.run_diagnostics import build_run_diagnostic_summary
36-
from src.market_phase_summary import extract_market_phase_summary
35+
from src.market_phase_summary import (
36+
extract_market_phase_summary,
37+
rebuild_market_phase_summary_for_stock_code,
38+
)
3739
from src.schemas.decision_action import build_action_fields
3840
from src.utils.sniper_points import find_sniper_points
3941
from src.utils.data_processing import (
@@ -281,18 +283,10 @@ def _display_stock_code(raw_code: Any) -> str:
281283
return resolve_index_stock_code(code) or code
282284

283285
def _display_market_phase_summary(self, stock_code: str, context_snapshot: Any) -> Any:
284-
summary = extract_market_phase_summary(context_snapshot)
285-
display_code = self._display_stock_code(stock_code)
286-
if not display_code or display_code == str(stock_code or "").strip():
287-
return summary
288-
289-
market = get_market_for_stock(display_code)
290-
if market not in {"jp", "kr"}:
291-
return summary
292-
293-
if not isinstance(summary, dict):
294-
return summary
295-
return {**summary, "market": market}
286+
return rebuild_market_phase_summary_for_stock_code(
287+
self._display_stock_code(stock_code),
288+
context_snapshot,
289+
)
296290

297291
def _record_to_list_item_dict(self, record) -> Dict[str, Any]:
298292
raw_result = parse_json_field(getattr(record, "raw_result", None))

src/services/stock_code_utils.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import re
99
from typing import Optional
1010

11-
from data_provider.base import is_bse_code
11+
from data_provider.base import canonical_stock_code, is_bse_code
1212

1313

1414
# Known exchange prefixes (case-insensitive) and the digit lengths they accept.
@@ -107,3 +107,28 @@ def normalize_code(raw: str) -> Optional[str]:
107107
if stripped is not None:
108108
return stripped
109109
return None
110+
111+
112+
def resolve_index_stock_code_for_analysis(raw: str) -> str:
113+
"""Resolve bare JP/KR candidates via stock index and keep suffix forms.
114+
115+
For code-like inputs:
116+
- Existing index-backed entries (e.g. ``005930`` -> ``005930.KS``) are
117+
preferred.
118+
- Non-matching code-like inputs keep the canonicalized input.
119+
120+
Non-code-like values are still canonicalized only, letting callers keep
121+
their own validation policy (e.g. API name resolution path).
122+
"""
123+
text = (raw or "").strip()
124+
if not text:
125+
return ""
126+
127+
if is_code_like(text):
128+
from src.data.stock_index_loader import resolve_index_stock_code
129+
130+
resolved = resolve_index_stock_code(text)
131+
if resolved:
132+
return canonical_stock_code(resolved)
133+
134+
return canonical_stock_code(text)

src/services/task_queue.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
reset_run_diagnostic_context,
3535
)
3636
from src.utils.analysis_metadata import SELECTION_SOURCES
37+
from src.services.stock_code_utils import resolve_index_stock_code_for_analysis
3738

3839
logger = logging.getLogger(__name__)
3940

@@ -45,7 +46,7 @@ def _dedupe_stock_code_key(stock_code: str) -> str:
4546
The task queue should treat equivalent market code shapes as the same
4647
underlying stock, e.g. ``600519`` and ``600519.SH``.
4748
"""
48-
return canonical_stock_code(normalize_stock_code(stock_code))
49+
return resolve_index_stock_code_for_analysis(normalize_stock_code(stock_code))
4950

5051

5152
class TaskStatus(str, Enum):
@@ -350,7 +351,7 @@ def submit_task(
350351
Raises:
351352
DuplicateTaskError: Raised when the stock is already being analyzed
352353
"""
353-
stock_code = canonical_stock_code(stock_code)
354+
stock_code = resolve_index_stock_code_for_analysis(stock_code)
354355
if not stock_code:
355356
raise ValueError("股票代码不能为空或仅包含空白字符")
356357

@@ -399,7 +400,7 @@ def submit_tasks_batch(
399400
created_task_ids: List[str] = []
400401

401402
canonical_codes = [
402-
normalized for normalized in (canonical_stock_code(code) for code in stock_codes)
403+
normalized for normalized in (resolve_index_stock_code_for_analysis(code) for code in stock_codes)
403404
if normalized
404405
]
405406

src/services/task_service.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from src.enums import ReportType
2424
from src.storage import get_db
2525
from bot.models import BotMessage
26+
from src.services.stock_code_utils import resolve_index_stock_code_for_analysis
2627

2728
logger = logging.getLogger(__name__)
2829

@@ -90,25 +91,32 @@ def submit_analysis(
9091
if isinstance(report_type, str):
9192
report_type = ReportType.from_str(report_type)
9293

93-
task_id = f"{code}_{datetime.now().strftime('%Y%m%d_%H%M%S_%f')}"
94+
normalized_code = resolve_index_stock_code_for_analysis(code)
95+
if not normalized_code:
96+
raise ValueError("股票代码不能为空或仅包含空白字符")
97+
98+
task_id = f"{normalized_code}_{datetime.now().strftime('%Y%m%d_%H%M%S_%f')}"
9499

95100
# 提交到线程池
96101
self.executor.submit(
97102
self._run_analysis,
98-
code,
103+
normalized_code,
99104
task_id,
100105
report_type,
101106
source_message,
102107
save_context_snapshot,
103108
query_source
104109
)
105110

106-
logger.info(f"[TaskService] 已提交股票 {code} 的分析任务, task_id={task_id}, report_type={report_type.value}")
111+
logger.info(
112+
f"[TaskService] 已提交股票 {normalized_code} 的分析任务, "
113+
f"task_id={task_id}, report_type={report_type.value}"
114+
)
107115

108116
return {
109117
"success": True,
110118
"message": "分析任务已提交,将异步执行并推送通知",
111-
"code": code,
119+
"code": normalized_code,
112120
"task_id": task_id,
113121
"report_type": report_type.value
114122
}

tests/test_analysis_api_contract.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1527,6 +1527,43 @@ def test_build_analysis_report_repairs_bare_kr_code_and_phase_summary(self) -> N
15271527
self.assertEqual(report.meta.market_phase_summary.trigger_source, "scheduled_job")
15281528
self.assertEqual(report.meta.market_phase_summary.analysis_intent, "postmarket")
15291529

1530+
def test_build_analysis_report_rebuilds_legacy_cn_market_summary_for_kr_code(self) -> None:
1531+
if _build_analysis_report is None:
1532+
self.skipTest("analysis endpoint helpers unavailable in this environment")
1533+
1534+
legacy_cn_summary = {
1535+
**_market_phase_summary(),
1536+
"market": "cn",
1537+
"phase": "intraday",
1538+
"market_local_time": "2026-03-27T10:00:00+08:00",
1539+
"session_date": "2026-03-27",
1540+
"effective_daily_bar_date": "2026-03-26",
1541+
"analysis_intent": "intraday",
1542+
"trigger_source": "history_snapshot",
1543+
"warnings": ["legacy_cn_snapshot"],
1544+
}
1545+
1546+
with patch("api.v1.endpoints.analysis.resolve_index_stock_code", return_value="005930.KS"):
1547+
report = _build_analysis_report(
1548+
report_data={
1549+
"meta": {"stock_code": "005930"},
1550+
"summary": {},
1551+
"strategy": {},
1552+
"details": {},
1553+
},
1554+
query_id="q-kr-legacy-cn",
1555+
stock_code="005930",
1556+
stock_name="三星电子",
1557+
context_snapshot={"market_phase_summary": legacy_cn_summary},
1558+
fallback_fundamental_payload=None,
1559+
)
1560+
1561+
self.assertIsNotNone(report.meta.market_phase_summary)
1562+
self.assertEqual(report.meta.stock_code, "005930.KS")
1563+
self.assertEqual(report.meta.market_phase_summary.market, "kr")
1564+
self.assertTrue(report.meta.market_phase_summary.market_local_time.endswith("+09:00"))
1565+
self.assertIn("legacy_cn_snapshot", report.meta.market_phase_summary.warnings)
1566+
15301567
def test_build_analysis_report_merges_partial_top_level_context_with_fallback(self) -> None:
15311568
if _build_analysis_report is None:
15321569
self.skipTest("analysis endpoint helpers unavailable in this environment")

0 commit comments

Comments
 (0)