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 __future__ import annotations
|
||||||
|
|
||||||
from datetime import date, datetime
|
from datetime import datetime
|
||||||
from typing import Any, Dict, List, Optional, Set, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from src.config import Config, get_config
|
from src.config import Config, get_config
|
||||||
from src.repositories.portfolio_repo import PortfolioRepository
|
from src.repositories.portfolio_repo import PortfolioRepository
|
||||||
@@ -61,7 +61,9 @@ class DashboardOverviewService:
|
|||||||
str,
|
str,
|
||||||
Dict[str, Optional[Dict[str, Any]]],
|
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]] = []
|
latest_reviews: List[Dict[str, Any]] = []
|
||||||
invalid_snapshot_count = 0
|
invalid_snapshot_count = 0
|
||||||
detail_failure_count = 0
|
detail_failure_count = 0
|
||||||
@@ -83,47 +85,58 @@ class DashboardOverviewService:
|
|||||||
review_count = int(result.get("total") or 0)
|
review_count = int(result.get("total") or 0)
|
||||||
if len(latest_reviews) < 5:
|
if len(latest_reviews) < 5:
|
||||||
latest_reviews.extend(reviews[: 5 - len(latest_reviews)])
|
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")
|
context_snapshot = review.get("context_snapshot")
|
||||||
if not isinstance(context_snapshot, dict):
|
if not isinstance(context_snapshot, dict):
|
||||||
detail_failure_count += 1
|
detail_failure_count += 1
|
||||||
|
global_detail_failure_ranks.append(review_rank)
|
||||||
continue
|
continue
|
||||||
snapshot_container = context_snapshot.get("market_light_snapshots")
|
snapshot_container = context_snapshot.get("market_light_snapshots")
|
||||||
if snapshot_container is not None and not isinstance(snapshot_container, dict):
|
if snapshot_container is not None and not isinstance(snapshot_container, dict):
|
||||||
detail_failure_count += 1
|
detail_failure_count += 1
|
||||||
|
global_detail_failure_ranks.append(review_rank)
|
||||||
continue
|
continue
|
||||||
for raw_region, raw_snapshot in (snapshot_container or {}).items():
|
for raw_region, raw_snapshot in (snapshot_container or {}).items():
|
||||||
region = str(raw_region).strip().lower()
|
region = str(raw_region).strip().lower()
|
||||||
if region and not isinstance(raw_snapshot, dict):
|
if region and not isinstance(raw_snapshot, dict):
|
||||||
invalid_snapshot_count += 1
|
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)
|
raw_snapshots = self._extract_snapshots(context_snapshot)
|
||||||
for region, raw_snapshot in raw_snapshots.items():
|
for region, raw_snapshot in raw_snapshots.items():
|
||||||
raw_trade_date = str(raw_snapshot.get("trade_date") or "").strip()
|
raw_trade_date = str(raw_snapshot.get("trade_date") or "").strip()
|
||||||
try:
|
try:
|
||||||
canonical_trade_date = date.fromisoformat(raw_trade_date).isoformat()
|
canonical_trade_date = self._canonical_trade_date(raw_trade_date)
|
||||||
except Exception:
|
except Exception:
|
||||||
invalid_snapshot_count += 1
|
invalid_snapshot_count += 1
|
||||||
regions_with_unknown_trade_date.add(region)
|
unknown_trade_date_failure_ranks_by_region.setdefault(region, []).append(
|
||||||
|
review_rank
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
region_ranks = snapshot_candidate_ranks_by_region.setdefault(region, {})
|
||||||
try:
|
try:
|
||||||
snapshot = MarketLightSnapshot.model_validate(raw_snapshot).model_dump()
|
snapshot = MarketLightSnapshot.model_validate(raw_snapshot).model_dump()
|
||||||
except Exception:
|
except Exception:
|
||||||
invalid_snapshot_count += 1
|
invalid_snapshot_count += 1
|
||||||
snapshot_candidates_by_region.setdefault(region, {}).setdefault(
|
region_candidates = snapshot_candidates_by_region.setdefault(region, {})
|
||||||
canonical_trade_date,
|
if canonical_trade_date not in region_candidates:
|
||||||
None,
|
region_candidates[canonical_trade_date] = None
|
||||||
)
|
region_ranks[canonical_trade_date] = review_rank
|
||||||
continue
|
continue
|
||||||
snapshot_region = str(snapshot.get("region") or "").strip().lower()
|
snapshot_region = str(snapshot.get("region") or "").strip().lower()
|
||||||
region_candidates = snapshot_candidates_by_region.setdefault(region, {})
|
region_candidates = snapshot_candidates_by_region.setdefault(region, {})
|
||||||
if snapshot_region != region:
|
if snapshot_region != region:
|
||||||
invalid_snapshot_count += 1
|
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
|
continue
|
||||||
snapshot["trade_date"] = canonical_trade_date
|
snapshot["trade_date"] = canonical_trade_date
|
||||||
if region_candidates.get(canonical_trade_date) is None:
|
if region_candidates.get(canonical_trade_date) is None:
|
||||||
region_candidates[canonical_trade_date] = snapshot
|
region_candidates[canonical_trade_date] = snapshot
|
||||||
|
region_ranks[canonical_trade_date] = review_rank
|
||||||
scanned_count += len(reviews)
|
scanned_count += len(reviews)
|
||||||
if scanned_count >= review_count:
|
if scanned_count >= review_count:
|
||||||
break
|
break
|
||||||
@@ -147,17 +160,25 @@ class DashboardOverviewService:
|
|||||||
unavailable_current_regions: List[str] = []
|
unavailable_current_regions: List[str] = []
|
||||||
for region, candidates in snapshot_candidates_by_region.items():
|
for region, candidates in snapshot_candidates_by_region.items():
|
||||||
ordered_dates = sorted(candidates, reverse=True)
|
ordered_dates = sorted(candidates, reverse=True)
|
||||||
if detail_failure_count or region in regions_with_unknown_trade_date:
|
candidate_ranks = snapshot_candidate_ranks_by_region.get(region, {})
|
||||||
unavailable_current_regions.append(region)
|
failure_ranks = [
|
||||||
continue
|
*global_detail_failure_ranks,
|
||||||
current = candidates[ordered_dates[0]] if ordered_dates else None
|
*unknown_trade_date_failure_ranks_by_region.get(region, []),
|
||||||
if current is None:
|
]
|
||||||
|
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)
|
unavailable_current_regions.append(region)
|
||||||
continue
|
continue
|
||||||
selected = [current]
|
selected = [current]
|
||||||
if len(ordered_dates) > 1:
|
if len(ordered_dates) > 1:
|
||||||
previous = candidates[ordered_dates[1]]
|
previous_date = ordered_dates[1]
|
||||||
if previous is not None:
|
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)
|
selected.append(previous)
|
||||||
snapshots_by_region[region] = selected
|
snapshots_by_region[region] = selected
|
||||||
|
|
||||||
@@ -203,6 +224,14 @@ class DashboardOverviewService:
|
|||||||
source_limitations=limitations,
|
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]:
|
def _personal_block(self) -> Dict[str, Any]:
|
||||||
data = {
|
data = {
|
||||||
"watchlist_count": None,
|
"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"]
|
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:
|
def test_malformed_region_snapshot_does_not_promote_older_region_snapshot() -> None:
|
||||||
dependencies = _dependencies()
|
dependencies = _dependencies()
|
||||||
original_detail = dependencies["history_service"].get_history_detail_by_id.side_effect
|
original_detail = dependencies["history_service"].get_history_detail_by_id.side_effect
|
||||||
|
|||||||
Reference in New Issue
Block a user