mirror of
https://github.com/ZhuLinsen/daily_stock_analysis
synced 2026-09-20 10:53:33 +08:00
fix: scope dashboard snapshot failures
This commit is contained in:
@@ -3,8 +3,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime
|
||||
from typing import Any, Dict, List, Optional, Set, Tuple
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from src.config import Config, get_config
|
||||
from src.repositories.portfolio_repo import PortfolioRepository
|
||||
@@ -61,7 +61,9 @@ class DashboardOverviewService:
|
||||
str,
|
||||
Dict[str, Optional[Dict[str, Any]]],
|
||||
] = {}
|
||||
regions_with_unknown_trade_date: Set[str] = set()
|
||||
snapshot_candidate_ranks_by_region: Dict[str, Dict[str, int]] = {}
|
||||
global_detail_failure_ranks: List[int] = []
|
||||
unknown_trade_date_failure_ranks_by_region: Dict[str, List[int]] = {}
|
||||
latest_reviews: List[Dict[str, Any]] = []
|
||||
invalid_snapshot_count = 0
|
||||
detail_failure_count = 0
|
||||
@@ -83,47 +85,58 @@ class DashboardOverviewService:
|
||||
review_count = int(result.get("total") or 0)
|
||||
if len(latest_reviews) < 5:
|
||||
latest_reviews.extend(reviews[: 5 - len(latest_reviews)])
|
||||
for review in reviews:
|
||||
for review_index, review in enumerate(reviews):
|
||||
review_rank = scanned_count + review_index
|
||||
context_snapshot = review.get("context_snapshot")
|
||||
if not isinstance(context_snapshot, dict):
|
||||
detail_failure_count += 1
|
||||
global_detail_failure_ranks.append(review_rank)
|
||||
continue
|
||||
snapshot_container = context_snapshot.get("market_light_snapshots")
|
||||
if snapshot_container is not None and not isinstance(snapshot_container, dict):
|
||||
detail_failure_count += 1
|
||||
global_detail_failure_ranks.append(review_rank)
|
||||
continue
|
||||
for raw_region, raw_snapshot in (snapshot_container or {}).items():
|
||||
region = str(raw_region).strip().lower()
|
||||
if region and not isinstance(raw_snapshot, dict):
|
||||
invalid_snapshot_count += 1
|
||||
regions_with_unknown_trade_date.add(region)
|
||||
unknown_trade_date_failure_ranks_by_region.setdefault(region, []).append(
|
||||
review_rank
|
||||
)
|
||||
raw_snapshots = self._extract_snapshots(context_snapshot)
|
||||
for region, raw_snapshot in raw_snapshots.items():
|
||||
raw_trade_date = str(raw_snapshot.get("trade_date") or "").strip()
|
||||
try:
|
||||
canonical_trade_date = date.fromisoformat(raw_trade_date).isoformat()
|
||||
canonical_trade_date = self._canonical_trade_date(raw_trade_date)
|
||||
except Exception:
|
||||
invalid_snapshot_count += 1
|
||||
regions_with_unknown_trade_date.add(region)
|
||||
unknown_trade_date_failure_ranks_by_region.setdefault(region, []).append(
|
||||
review_rank
|
||||
)
|
||||
continue
|
||||
region_ranks = snapshot_candidate_ranks_by_region.setdefault(region, {})
|
||||
try:
|
||||
snapshot = MarketLightSnapshot.model_validate(raw_snapshot).model_dump()
|
||||
except Exception:
|
||||
invalid_snapshot_count += 1
|
||||
snapshot_candidates_by_region.setdefault(region, {}).setdefault(
|
||||
canonical_trade_date,
|
||||
None,
|
||||
)
|
||||
region_candidates = snapshot_candidates_by_region.setdefault(region, {})
|
||||
if canonical_trade_date not in region_candidates:
|
||||
region_candidates[canonical_trade_date] = None
|
||||
region_ranks[canonical_trade_date] = review_rank
|
||||
continue
|
||||
snapshot_region = str(snapshot.get("region") or "").strip().lower()
|
||||
region_candidates = snapshot_candidates_by_region.setdefault(region, {})
|
||||
if snapshot_region != region:
|
||||
invalid_snapshot_count += 1
|
||||
region_candidates.setdefault(canonical_trade_date, None)
|
||||
if canonical_trade_date not in region_candidates:
|
||||
region_candidates[canonical_trade_date] = None
|
||||
region_ranks[canonical_trade_date] = review_rank
|
||||
continue
|
||||
snapshot["trade_date"] = canonical_trade_date
|
||||
if region_candidates.get(canonical_trade_date) is None:
|
||||
region_candidates[canonical_trade_date] = snapshot
|
||||
region_ranks[canonical_trade_date] = review_rank
|
||||
scanned_count += len(reviews)
|
||||
if scanned_count >= review_count:
|
||||
break
|
||||
@@ -147,17 +160,25 @@ class DashboardOverviewService:
|
||||
unavailable_current_regions: List[str] = []
|
||||
for region, candidates in snapshot_candidates_by_region.items():
|
||||
ordered_dates = sorted(candidates, reverse=True)
|
||||
if detail_failure_count or region in regions_with_unknown_trade_date:
|
||||
unavailable_current_regions.append(region)
|
||||
continue
|
||||
current = candidates[ordered_dates[0]] if ordered_dates else None
|
||||
if current is None:
|
||||
candidate_ranks = snapshot_candidate_ranks_by_region.get(region, {})
|
||||
failure_ranks = [
|
||||
*global_detail_failure_ranks,
|
||||
*unknown_trade_date_failure_ranks_by_region.get(region, []),
|
||||
]
|
||||
current_date = ordered_dates[0] if ordered_dates else None
|
||||
current = candidates[current_date] if current_date else None
|
||||
current_rank = candidate_ranks.get(current_date, scanned_count) if current_date else scanned_count
|
||||
if current is None or any(rank < current_rank for rank in failure_ranks):
|
||||
unavailable_current_regions.append(region)
|
||||
continue
|
||||
selected = [current]
|
||||
if len(ordered_dates) > 1:
|
||||
previous = candidates[ordered_dates[1]]
|
||||
if previous is not None:
|
||||
previous_date = ordered_dates[1]
|
||||
previous = candidates[previous_date]
|
||||
previous_rank = candidate_ranks.get(previous_date, scanned_count)
|
||||
if previous is not None and not any(
|
||||
rank < previous_rank for rank in failure_ranks
|
||||
):
|
||||
selected.append(previous)
|
||||
snapshots_by_region[region] = selected
|
||||
|
||||
@@ -203,6 +224,14 @@ class DashboardOverviewService:
|
||||
source_limitations=limitations,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _canonical_trade_date(raw_trade_date: str) -> str:
|
||||
"""Parse supported ISO date forms consistently on Python 3.10+."""
|
||||
raw = str(raw_trade_date or "").strip()
|
||||
compact = len(raw) == 8 and raw.isascii() and raw.isdigit()
|
||||
date_format = "%Y%m%d" if compact else "%Y-%m-%d"
|
||||
return datetime.strptime(raw, date_format).date().isoformat()
|
||||
|
||||
def _personal_block(self) -> Dict[str, Any]:
|
||||
data = {
|
||||
"watchlist_count": None,
|
||||
|
||||
@@ -384,6 +384,76 @@ def test_malformed_nested_snapshot_container_does_not_promote_older_snapshots()
|
||||
assert "latest_completed_snapshot_unavailable" in payload["market"]["meta"]["limitations"]
|
||||
|
||||
|
||||
def test_older_malformed_detail_does_not_hide_valid_current_and_baseline() -> None:
|
||||
dependencies = _dependencies()
|
||||
reviews = [
|
||||
_review(1, "2026-08-29T09:00:00+08:00"),
|
||||
_review(2, "2026-08-28T09:00:00+08:00"),
|
||||
_review(3, "2026-08-27T09:00:00+08:00"),
|
||||
]
|
||||
dependencies["history_service"].get_history_list.side_effect = lambda **kwargs: (
|
||||
_history_page(dependencies["history_service"], reviews, kwargs)
|
||||
if kwargs.get("report_type") == "market_review"
|
||||
else {"items": [], "total": 0}
|
||||
)
|
||||
|
||||
def detail(record_id: int) -> dict:
|
||||
if record_id == 3:
|
||||
return {"context_snapshot": "{invalid-json"}
|
||||
trade_date = "2026-08-29" if record_id == 1 else "2026-08-28"
|
||||
return {
|
||||
"context_snapshot": {
|
||||
"market_light_snapshots": {
|
||||
"cn": _snapshot("cn", trade_date, 60 if record_id == 1 else 50, "yellow")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies["history_service"].get_history_detail_by_id.side_effect = detail
|
||||
|
||||
payload = DashboardOverviewService(**dependencies).get_overview()
|
||||
|
||||
assert payload["market"]["data"]["latest_snapshots"]["cn"]["trade_date"] == "2026-08-29"
|
||||
assert payload["what_changed"]["data"]["current_trade_dates"] == {"cn": "2026-08-29"}
|
||||
assert payload["what_changed"]["data"]["previous_trade_dates"] == {"cn": "2026-08-28"}
|
||||
assert "market_review_detail_partial" in payload["market"]["meta"]["limitations"]
|
||||
|
||||
|
||||
def test_malformed_detail_between_candidates_blocks_only_the_baseline() -> None:
|
||||
dependencies = _dependencies()
|
||||
reviews = [
|
||||
_review(1, "2026-08-29T09:00:00+08:00"),
|
||||
_review(2, "2026-08-28T09:00:00+08:00"),
|
||||
_review(3, "2026-08-27T09:00:00+08:00"),
|
||||
]
|
||||
dependencies["history_service"].get_history_list.side_effect = lambda **kwargs: (
|
||||
_history_page(dependencies["history_service"], reviews, kwargs)
|
||||
if kwargs.get("report_type") == "market_review"
|
||||
else {"items": [], "total": 0}
|
||||
)
|
||||
|
||||
def detail(record_id: int) -> dict:
|
||||
if record_id == 2:
|
||||
return {"context_snapshot": "{invalid-json"}
|
||||
trade_date = "2026-08-29" if record_id == 1 else "2026-08-27"
|
||||
return {
|
||||
"context_snapshot": {
|
||||
"market_light_snapshots": {
|
||||
"cn": _snapshot("cn", trade_date, 60 if record_id == 1 else 40, "yellow")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies["history_service"].get_history_detail_by_id.side_effect = detail
|
||||
|
||||
payload = DashboardOverviewService(**dependencies).get_overview()
|
||||
|
||||
assert payload["market"]["data"]["latest_snapshots"]["cn"]["trade_date"] == "2026-08-29"
|
||||
assert payload["what_changed"]["data"]["current_trade_dates"] == {"cn": "2026-08-29"}
|
||||
assert payload["what_changed"]["data"]["previous_trade_dates"] == {}
|
||||
assert "previous_completed_snapshot_unavailable:cn" in payload["what_changed"]["meta"]["limitations"]
|
||||
|
||||
|
||||
def test_malformed_region_snapshot_does_not_promote_older_region_snapshot() -> None:
|
||||
dependencies = _dependencies()
|
||||
original_detail = dependencies["history_service"].get_history_detail_by_id.side_effect
|
||||
|
||||
Reference in New Issue
Block a user