refactor: centralize compact date formatting

This commit is contained in:
leefer
2026-08-02 00:18:38 +08:00
parent 2ef31f6115
commit 309ed277fe
9 changed files with 62 additions and 14 deletions
+2
View File
@@ -28,6 +28,8 @@ background scheduler
- `server.py` is the stable command/import facade. Runtime composition lives in - `server.py` is the stable command/import facade. Runtime composition lives in
`backend/application.py` and `backend/bootstrap/`. `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 - `backend/http/` owns common authentication, request IDs, responses, static delivery, and
error normalization. Feature-specific transport handlers live beside their feature. 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 Exact POST endpoints that only delegate to one of those handlers use the explicit maps in
+4
View File
@@ -71,6 +71,10 @@ def normalize_date(value: str) -> str:
return parsed.strftime("%Y%m%d") 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: def validate_stock_code(value: str) -> str:
code = value.strip() code = value.strip()
if not re.fullmatch(r"\d{6}", code): if not re.fullmatch(r"\d{6}", code):
+1 -4
View File
@@ -11,6 +11,7 @@ from datetime import datetime, time as dt_time, timedelta
from threading import Lock from threading import Lock
from typing import Any, ClassVar 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.data.numbers import finite_number as _number
from backend.features.sentiment.engine import apply_sentiment_to_dashboard 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]}" 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: def _realtime_market_status(current_time: dt_time) -> str:
if current_time < dt_time(9, 25): if current_time < dt_time(9, 25):
return "pre_open" return "pre_open"
+1 -4
View File
@@ -8,6 +8,7 @@ from collections import defaultdict
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any 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.numbers import finite_number as _number
from backend.data.providers.tushare_client import TushareClient, TushareError from backend.data.providers.tushare_client import TushareClient, TushareError
from backend.features.screener.strategies import ADVANCED_CURATED_STRATEGIES from backend.features.screener.strategies import ADVANCED_CURATED_STRATEGIES
@@ -2200,7 +2201,3 @@ def _regime_reason(regime: str) -> str:
"divergence": "指数或核心仍强,但广度、封板质量开始分化。", "divergence": "指数或核心仍强,但广度、封板质量开始分化。",
"retreat": "情绪指标继续走弱,应提高筛选门槛并接受无候选结果。", "retreat": "情绪指标继续走弱,应提高筛选门槛并接受无候选结果。",
}.get(regime, "市场阶段待确认。") }.get(regime, "市场阶段待确认。")
def _display_date(value: str) -> str:
return f"{value[:4]}-{value[4:6]}-{value[6:8]}" if len(value) == 8 else value
+10 -4
View File
@@ -230,6 +230,12 @@
"path": "backend/data/numbers.py" "path": "backend/data/numbers.py"
} }
], ],
"date_formatting": [
{
"function": "display_compact_date",
"path": "backend/bootstrap/config.py"
}
],
"llm_entrypoints": [ "llm_entrypoints": [
{ {
"function": "stream_with_mentor", "function": "stream_with_mentor",
@@ -289,13 +295,13 @@
}, },
{ {
"path": "backend/features/screener/engine.py", "path": "backend/features/screener/engine.py",
"bytes": 108434, "bytes": 108387,
"lines": 2206 "lines": 2203
}, },
{ {
"path": "backend/data/providers/tushare_client.py", "path": "backend/data/providers/tushare_client.py",
"bytes": 94171, "bytes": 94124,
"lines": 2168 "lines": 2165
}, },
{ {
"path": "frontend/app.js", "path": "frontend/app.js",
@@ -146,6 +146,7 @@ class MarketSliceSourceEquivalenceTests(unittest.TestCase):
self.assertEqual(sha256(ORIGINAL_ROOT / original), sha256(APP_ROOT / migrated)) self.assertEqual(sha256(ORIGINAL_ROOT / original), sha256(APP_ROOT / migrated))
original_tushare = top_level_definitions(ORIGINAL_ROOT / "tushare_client.py") original_tushare = top_level_definitions(ORIGINAL_ROOT / "tushare_client.py")
original_tushare.pop("_number") original_tushare.pop("_number")
original_tushare.pop("_display_date")
self.assertEqual( self.assertEqual(
original_tushare, original_tushare,
top_level_definitions(APP_ROOT / "backend/data/providers/tushare_client.py"), 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"), function_contract(APP_ROOT / "backend/data/numbers.py", "finite_number"),
) )
self.assertIs(canonical_tushare._number, 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 = top_level_definitions(ORIGINAL_ROOT / "chart_data_provider.py")
original_charts.pop("_stock_market_code") original_charts.pop("_stock_market_code")
self.assertEqual( self.assertEqual(
+10 -2
View File
@@ -9,6 +9,7 @@ import advanced_strategies
import llm_strategy import llm_strategy
import screener import screener
import strategy_tracking import strategy_tracking
from backend.bootstrap import config as bootstrap_config
from backend.data.numbers import finite_number from backend.data.numbers import finite_number
from backend.features.screener import compiler, engine, strategies, tracking from backend.features.screener import compiler, engine, strategies, tracking
from backend.features.screener import service as screener_service from backend.features.screener import service as screener_service
@@ -151,12 +152,12 @@ class ScreenerSliceSourceEquivalenceTests(unittest.TestCase):
self.assertEqual( self.assertEqual(
module_contract( module_contract(
ORIGINAL_ROOT / "screener.py", ORIGINAL_ROOT / "screener.py",
excluded_definitions={"_number"}, excluded_definitions={"_display_date", "_number"},
exclude_imports=True, exclude_imports=True,
), ),
module_contract( module_contract(
APP_ROOT / "backend" / "features" / "screener" / "engine.py", APP_ROOT / "backend" / "features" / "screener" / "engine.py",
excluded_definitions={"_number"}, excluded_definitions={"_display_date", "_number"},
exclude_imports=True, exclude_imports=True,
), ),
) )
@@ -165,6 +166,13 @@ class ScreenerSliceSourceEquivalenceTests(unittest.TestCase):
function_contract(APP_ROOT / "backend/data/numbers.py", "finite_number"), function_contract(APP_ROOT / "backend/data/numbers.py", "finite_number"),
) )
self.assertIs(engine._number, 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( self.assertEqual(
class_methods( class_methods(
ORIGINAL_ROOT / "backend" / "features" / "screener" / "tracking.py", ORIGINAL_ROOT / "backend" / "features" / "screener" / "tracking.py",
@@ -159,6 +159,9 @@ def build() -> dict[str, Any]:
{"function": "finite_number", "path": "backend/data/numbers.py"}, {"function": "finite_number", "path": "backend/data/numbers.py"},
{"function": "non_nan_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": [ "llm_entrypoints": [
{"function": "stream_with_mentor", "path": "backend/features/mentor/agent.py"}, {"function": "stream_with_mentor", "path": "backend/features/mentor/agent.py"},
{"function": "interpret_heaven", "path": "backend/features/heaven/agent.py"}, {"function": "interpret_heaven", "path": "backend/features/heaven/agent.py"},
+23
View File
@@ -23,6 +23,7 @@
| CR-03 | 股票市场后缀转换 | Tushare业务与iFinD图表各保留一份完全相同的沪深京代码转换函数 | 图表复用`bootstrap/config.py::tushare_code`,只保留一份函数体 | 已完成 | | CR-03 | 股票市场后缀转换 | Tushare业务与iFinD图表各保留一份完全相同的沪深京代码转换函数 | 图表复用`bootstrap/config.py::tushare_code`,只保留一份函数体 | 已完成 |
| CR-04 | 数值归一化策略 | 四个业务模块分别保留两组完全相同的数值转换函数体 | 由`backend/data/numbers.py`集中拥有两种既有语义,消费者保留原局部别名 | 已完成 | | CR-04 | 数值归一化策略 | 四个业务模块分别保留两组完全相同的数值转换函数体 | 由`backend/data/numbers.py`集中拥有两种既有语义,消费者保留原局部别名 | 已完成 |
| CR-05 | 根级兼容入口 | 正式后端仍有五处通过迁移兼容模块反向导入规范实现 | 正式代码改用规范路径;兼容入口只服务原公开导入契约 | 已完成 | | CR-05 | 根级兼容入口 | 正式后端仍有五处通过迁移兼容模块反向导入规范实现 | 正式代码改用规范路径;兼容入口只服务原公开导入契约 | 已完成 |
| CR-06 | 紧凑日期显示 | Tushare与选股引擎各保留一份完全相同的`YYYYMMDD`显示转换 | 由`bootstrap/config.py`拥有唯一格式策略,消费者保留原局部别名 | 已完成 |
## CR-01验收口径 ## CR-01验收口径
@@ -139,6 +140,28 @@
本批基线为`xiaobai-reduction-04-numeric-normalization-20260801`;检查点为 本批基线为`xiaobai-reduction-04-numeric-normalization-20260801`;检查点为
`xiaobai-reduction-05-compatibility-boundaries-20260801` `xiaobai-reduction-05-compatibility-boundaries-20260801`
## CR-06验收口径
- 只合并参数、函数体和异常行为完全相同的日期/文本转换;名称相似但空值、未来日期、错误文案或
输入格式不同的函数不得合并。
- Tushare与选股引擎继续暴露局部`_display_date`名称,并分别指向唯一共享实现。
- 原版两个`_display_date`函数必须分别与共享实现AST相等,所有原调用结果保持不变。
- 市场洞察的日期显示函数会清理连字符并容忍空值,语义不同,必须继续独立保留。
## CR-06结果
- 删除Tushare与选股引擎内两个重复日期函数体,新增`display_compact_date`唯一策略;生产代码总
行数不增加,重复函数体由两份降为一份。
- 架构清单登记日期格式唯一所有权;保持性测试改为未改范围AST相等、共享函数AST相等和运行时
对象身份三重契约,没有放宽原迁移门禁。
- `normalize_date`、市场洞察日期显示、实时行情时间格式和会员日期边界因语义不同均原样保留。
- 候选321项、纯`app/`导出258项、24个JavaScript文件、API/架构注册表、Git空白检查和SQLite
完整性检查通过;本批不涉及页面、CSS或浏览器行为。
- 本批不修改日期输入规则、业务计算、选股结果、接口、数据库、数据源、LLM、权限或部署。
本批基线为`xiaobai-reduction-05-compatibility-boundaries-20260801`;检查点为
`xiaobai-reduction-06-date-formatting-20260801`
## 人工验收记录 ## 人工验收记录
- 2026-08-01:用户检查CR-02与CR-03运行结果,确认未发现明显异常。本记录仅表示本轮可见功能与 - 2026-08-01:用户检查CR-02与CR-03运行结果,确认未发现明显异常。本记录仅表示本轮可见功能与