diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index d4d65c7..8c6cb78 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -28,6 +28,8 @@ background scheduler - `server.py` is the stable command/import facade. Runtime composition lives in `backend/application.py` and `backend/bootstrap/`. +- `backend/bootstrap/` owns process configuration, dependency construction, startup, and + shared input/display-format contracts. It does not own feature behavior. - `backend/http/` owns common authentication, request IDs, responses, static delivery, and error normalization. Feature-specific transport handlers live beside their feature. Exact POST endpoints that only delegate to one of those handlers use the explicit maps in diff --git a/backend/bootstrap/config.py b/backend/bootstrap/config.py index 243ebb6..58813bd 100644 --- a/backend/bootstrap/config.py +++ b/backend/bootstrap/config.py @@ -71,6 +71,10 @@ def normalize_date(value: str) -> str: return parsed.strftime("%Y%m%d") +def display_compact_date(value: str) -> str: + return f"{value[:4]}-{value[4:6]}-{value[6:8]}" if len(value) == 8 else value + + def validate_stock_code(value: str) -> str: code = value.strip() if not re.fullmatch(r"\d{6}", code): diff --git a/backend/data/providers/tushare_client.py b/backend/data/providers/tushare_client.py index 752708a..4c071b9 100644 --- a/backend/data/providers/tushare_client.py +++ b/backend/data/providers/tushare_client.py @@ -11,6 +11,7 @@ from datetime import datetime, time as dt_time, timedelta from threading import Lock from typing import Any, ClassVar +from backend.bootstrap.config import display_compact_date as _display_date from backend.data.numbers import finite_number as _number from backend.features.sentiment.engine import apply_sentiment_to_dashboard @@ -2007,10 +2008,6 @@ def _display_time(value: Any) -> str: return f"{raw[:2]}:{raw[2:4]}:{raw[4:6]}" -def _display_date(value: str) -> str: - return f"{value[:4]}-{value[4:6]}-{value[6:8]}" if len(value) == 8 else value - - def _realtime_market_status(current_time: dt_time) -> str: if current_time < dt_time(9, 25): return "pre_open" diff --git a/backend/features/screener/engine.py b/backend/features/screener/engine.py index 702f5bb..7d7baf9 100644 --- a/backend/features/screener/engine.py +++ b/backend/features/screener/engine.py @@ -8,6 +8,7 @@ from collections import defaultdict from datetime import datetime, timedelta from typing import Any +from backend.bootstrap.config import display_compact_date as _display_date from backend.data.numbers import finite_number as _number from backend.data.providers.tushare_client import TushareClient, TushareError from backend.features.screener.strategies import ADVANCED_CURATED_STRATEGIES @@ -2200,7 +2201,3 @@ def _regime_reason(regime: str) -> str: "divergence": "指数或核心仍强,但广度、封板质量开始分化。", "retreat": "情绪指标继续走弱,应提高筛选门槛并接受无候选结果。", }.get(regime, "市场阶段待确认。") - - -def _display_date(value: str) -> str: - return f"{value[:4]}-{value[4:6]}-{value[6:8]}" if len(value) == 8 else value diff --git a/config/architecture-inventory.json b/config/architecture-inventory.json index f16e44f..5941ac4 100644 --- a/config/architecture-inventory.json +++ b/config/architecture-inventory.json @@ -230,6 +230,12 @@ "path": "backend/data/numbers.py" } ], + "date_formatting": [ + { + "function": "display_compact_date", + "path": "backend/bootstrap/config.py" + } + ], "llm_entrypoints": [ { "function": "stream_with_mentor", @@ -289,13 +295,13 @@ }, { "path": "backend/features/screener/engine.py", - "bytes": 108434, - "lines": 2206 + "bytes": 108387, + "lines": 2203 }, { "path": "backend/data/providers/tushare_client.py", - "bytes": 94171, - "lines": 2168 + "bytes": 94124, + "lines": 2165 }, { "path": "frontend/app.js", diff --git a/tests/test_preservation_slice_market.py b/tests/test_preservation_slice_market.py index eae887a..fa3f54c 100644 --- a/tests/test_preservation_slice_market.py +++ b/tests/test_preservation_slice_market.py @@ -146,6 +146,7 @@ class MarketSliceSourceEquivalenceTests(unittest.TestCase): self.assertEqual(sha256(ORIGINAL_ROOT / original), sha256(APP_ROOT / migrated)) original_tushare = top_level_definitions(ORIGINAL_ROOT / "tushare_client.py") original_tushare.pop("_number") + original_tushare.pop("_display_date") self.assertEqual( original_tushare, top_level_definitions(APP_ROOT / "backend/data/providers/tushare_client.py"), @@ -155,6 +156,13 @@ class MarketSliceSourceEquivalenceTests(unittest.TestCase): function_contract(APP_ROOT / "backend/data/numbers.py", "finite_number"), ) self.assertIs(canonical_tushare._number, finite_number) + self.assertEqual( + function_contract(ORIGINAL_ROOT / "tushare_client.py", "_display_date"), + function_contract( + APP_ROOT / "backend/bootstrap/config.py", "display_compact_date" + ), + ) + self.assertIs(canonical_tushare._display_date, bootstrap_config.display_compact_date) original_charts = top_level_definitions(ORIGINAL_ROOT / "chart_data_provider.py") original_charts.pop("_stock_market_code") self.assertEqual( diff --git a/tests/test_preservation_slice_screener.py b/tests/test_preservation_slice_screener.py index 2788981..a8bbdac 100644 --- a/tests/test_preservation_slice_screener.py +++ b/tests/test_preservation_slice_screener.py @@ -9,6 +9,7 @@ import advanced_strategies import llm_strategy import screener import strategy_tracking +from backend.bootstrap import config as bootstrap_config from backend.data.numbers import finite_number from backend.features.screener import compiler, engine, strategies, tracking from backend.features.screener import service as screener_service @@ -151,12 +152,12 @@ class ScreenerSliceSourceEquivalenceTests(unittest.TestCase): self.assertEqual( module_contract( ORIGINAL_ROOT / "screener.py", - excluded_definitions={"_number"}, + excluded_definitions={"_display_date", "_number"}, exclude_imports=True, ), module_contract( APP_ROOT / "backend" / "features" / "screener" / "engine.py", - excluded_definitions={"_number"}, + excluded_definitions={"_display_date", "_number"}, exclude_imports=True, ), ) @@ -165,6 +166,13 @@ class ScreenerSliceSourceEquivalenceTests(unittest.TestCase): function_contract(APP_ROOT / "backend/data/numbers.py", "finite_number"), ) self.assertIs(engine._number, finite_number) + self.assertEqual( + function_contract(ORIGINAL_ROOT / "screener.py", "_display_date"), + function_contract( + APP_ROOT / "backend/bootstrap/config.py", "display_compact_date" + ), + ) + self.assertIs(engine._display_date, bootstrap_config.display_compact_date) self.assertEqual( class_methods( ORIGINAL_ROOT / "backend" / "features" / "screener" / "tracking.py", diff --git a/tools/build_architecture_inventory.py b/tools/build_architecture_inventory.py index 8dbb448..f89a766 100644 --- a/tools/build_architecture_inventory.py +++ b/tools/build_architecture_inventory.py @@ -159,6 +159,9 @@ def build() -> dict[str, Any]: {"function": "finite_number", "path": "backend/data/numbers.py"}, {"function": "non_nan_number", "path": "backend/data/numbers.py"}, ], + "date_formatting": [ + {"function": "display_compact_date", "path": "backend/bootstrap/config.py"}, + ], "llm_entrypoints": [ {"function": "stream_with_mentor", "path": "backend/features/mentor/agent.py"}, {"function": "interpret_heaven", "path": "backend/features/heaven/agent.py"},