Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
309ed277fe | ||
|
|
2ef31f6115 |
+7
-1
@@ -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
|
||||
@@ -55,7 +57,11 @@ background scheduler
|
||||
|
||||
Root modules such as `screener.py`, `tushare_client.py`, and `mentor_agent.py` are compatibility
|
||||
aliases to canonical modules. They contain no second implementation and remain only because
|
||||
the original public import surface is part of the preservation contract.
|
||||
the original public import surface is part of the preservation contract. Canonical backend
|
||||
modules must import other canonical modules directly rather than routing through these aliases.
|
||||
The remaining `api_access` import in `backend/application.py` and preserved lazy
|
||||
`sentiment_engine` import in the screener repository are registered transition boundaries;
|
||||
the root `database.py` remains the documented schema/composition anchor.
|
||||
|
||||
## Non-negotiable maintenance rules
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -9,10 +9,10 @@ from backend.database.repositories import RepositoryBundle, build_repository_bun
|
||||
from backend.features.alerts import AlertService
|
||||
from backend.features.mentor.agent import MentorSkillRegistry
|
||||
from backend.features.review import TradeJournalService
|
||||
from backend.features.screener.engine import ScreenerEngine
|
||||
from backend.features.screener.tracking import StrategyTrackingService
|
||||
from backend.jobs import InProcessJobRunner, JobRegistry, SQLiteJobRunRepository
|
||||
from database import ReviewDatabase
|
||||
from screener import ScreenerEngine
|
||||
from backend.data.providers.ifind_client import IfindHttpClient
|
||||
from backend.data.realtime import WebRealtimeAggregator
|
||||
from backend.features.market.charts import MarketChartClient
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -4,7 +4,7 @@ import json
|
||||
from typing import Any
|
||||
|
||||
from backend.llm import transport as llm_transport
|
||||
from screener import FACTOR_FIELDS, REGIMES
|
||||
from backend.features.screener.engine import FACTOR_FIELDS, REGIMES
|
||||
|
||||
|
||||
class LLMCompilerError(RuntimeError):
|
||||
|
||||
@@ -8,11 +8,12 @@ from collections import defaultdict
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
from advanced_strategies import ADVANCED_CURATED_STRATEGIES
|
||||
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
|
||||
from database import ReviewDatabase
|
||||
from backend.features.sentiment.engine import build_sentiment_history, latest_contiguous_history
|
||||
from tushare_client import TushareClient, TushareError
|
||||
|
||||
|
||||
REGIMES = {
|
||||
@@ -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
|
||||
|
||||
@@ -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": 108394,
|
||||
"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",
|
||||
|
||||
+2
-1
@@ -3,7 +3,8 @@ from __future__ import annotations
|
||||
import argparse
|
||||
from datetime import date
|
||||
|
||||
from server import SERVICE, normalize_date
|
||||
from backend.application import SERVICE
|
||||
from backend.bootstrap.config import normalize_date
|
||||
|
||||
|
||||
def main() -> None:
|
||||
|
||||
@@ -20,12 +20,6 @@ class FeatureBoundaryTests(unittest.TestCase):
|
||||
}
|
||||
violations = []
|
||||
for path in FEATURES.rglob("*.py"):
|
||||
# The screener engine is an exact-preservation move of the legacy
|
||||
# calculation module. Its provider dependency is covered by the
|
||||
# slice equivalence tests and will be addressed only after the
|
||||
# behavior-preserving migration is complete.
|
||||
if path.relative_to(FEATURES).as_posix() == "screener/engine.py":
|
||||
continue
|
||||
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
||||
for node in ast.walk(tree):
|
||||
names = []
|
||||
@@ -38,6 +32,53 @@ class FeatureBoundaryTests(unittest.TestCase):
|
||||
violations.append(f"{path.relative_to(ROOT)} -> {name}")
|
||||
self.assertEqual(violations, [])
|
||||
|
||||
def test_backend_uses_root_compatibility_modules_only_at_declared_boundaries(self) -> None:
|
||||
compatibility_modules = {
|
||||
"advanced_strategies",
|
||||
"alert_service",
|
||||
"api_access",
|
||||
"app_config",
|
||||
"assistant_agent",
|
||||
"chart_data_provider",
|
||||
"heaven_agent",
|
||||
"heaven_engine",
|
||||
"ifind_client",
|
||||
"llm_strategy",
|
||||
"llm_stream",
|
||||
"market_insights",
|
||||
"mentor_agent",
|
||||
"realtime_aggregator",
|
||||
"screener",
|
||||
"security",
|
||||
"sentiment_engine",
|
||||
"server",
|
||||
"strategy_tracking",
|
||||
"trade_journal",
|
||||
"tushare_client",
|
||||
}
|
||||
allowed = {
|
||||
"backend/application.py": {"api_access"},
|
||||
"backend/features/screener/repository.py": {"sentiment_engine"},
|
||||
}
|
||||
violations = []
|
||||
for path in (ROOT / "backend").rglob("*.py"):
|
||||
relative = path.relative_to(ROOT).as_posix()
|
||||
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
||||
for node in ast.walk(tree):
|
||||
names = []
|
||||
if isinstance(node, ast.Import):
|
||||
names = [alias.name for alias in node.names]
|
||||
elif isinstance(node, ast.ImportFrom) and node.module:
|
||||
names = [node.module]
|
||||
for name in names:
|
||||
root_name = name.split(".")[0]
|
||||
if (
|
||||
root_name in compatibility_modules
|
||||
and root_name not in allowed.get(relative, set())
|
||||
):
|
||||
violations.append(f"{relative} -> {name}")
|
||||
self.assertEqual(violations, [])
|
||||
|
||||
def test_legacy_service_modules_are_compatibility_exports_only(self) -> None:
|
||||
for filename in ("alert_service.py", "trade_journal.py", "strategy_tracking.py"):
|
||||
tree = ast.parse((ROOT / filename).read_text(encoding="utf-8"))
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
| CR-02 | HTTP精确POST委托 | 27个端点重复使用“比较路径、调用无参数处理器、返回”三行分支 | 用公开/受保护两张显式映射统一委托,同时保留复杂路由的原控制流 | 已完成 |
|
||||
| CR-03 | 股票市场后缀转换 | Tushare业务与iFinD图表各保留一份完全相同的沪深京代码转换函数 | 图表复用`bootstrap/config.py::tushare_code`,只保留一份函数体 | 已完成 |
|
||||
| CR-04 | 数值归一化策略 | 四个业务模块分别保留两组完全相同的数值转换函数体 | 由`backend/data/numbers.py`集中拥有两种既有语义,消费者保留原局部别名 | 已完成 |
|
||||
| CR-05 | 根级兼容入口 | 正式后端仍有五处通过迁移兼容模块反向导入规范实现 | 正式代码改用规范路径;兼容入口只服务原公开导入契约 | 已完成 |
|
||||
| CR-06 | 紧凑日期显示 | Tushare与选股引擎各保留一份完全相同的`YYYYMMDD`显示转换 | 由`bootstrap/config.py`拥有唯一格式策略,消费者保留原局部别名 | 已完成 |
|
||||
|
||||
## CR-01验收口径
|
||||
|
||||
@@ -115,6 +117,51 @@
|
||||
本批基线为`xiaobai-reduction-03-market-symbol-20260801`;检查点为
|
||||
`xiaobai-reduction-04-numeric-normalization-20260801`。
|
||||
|
||||
## CR-05验收口径
|
||||
|
||||
- 逐项扫描根级Python入口、生产代码、测试、工具和动态导入;没有消费者或兼容责任的入口才能删除。
|
||||
- 规范后端不得经由`screener`、`advanced_strategies`、`tushare_client`或`server`兼容入口
|
||||
间接访问已经归位的实现。
|
||||
- 所有根级模块继续保持原导入名称、导出对象及模块对象身份,既有启动命令和第三方维护脚本不受影响。
|
||||
- `api_access`、选股Repository的惰性`sentiment_engine`导入及根级`database.py`属于已登记边界,
|
||||
分别留到HTTP、Repository阶段处理,不在本批跨边界修改。
|
||||
|
||||
## CR-05结果
|
||||
|
||||
- 审计确认21个根级兼容入口均有测试、工具、启动或原公开导入契约消费者,因此本批没有冒险删除文件。
|
||||
- 容器、策略编译器、选股引擎及数据同步命令的五处导入改为规范模块路径,正式代码不再通过四个根级
|
||||
兼容模块反向进入实现;运行代码行数未增加。
|
||||
- 特性边界测试取消选股引擎旧例外,并新增全后端兼容导入门禁;只允许两项已登记过渡边界,后续代码
|
||||
无法重新引入隐式根级依赖。
|
||||
- 候选321项、纯`app/`导出258项、24个JavaScript文件、API/架构注册表、Git空白检查和SQLite
|
||||
完整性检查通过;本批不涉及页面、CSS或浏览器行为。
|
||||
- 本批不修改业务计算、策略公式、数据源、API、数据库、LLM、权限、前端或部署。
|
||||
|
||||
本批基线为`xiaobai-reduction-04-numeric-normalization-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运行结果,确认未发现明显异常。本记录仅表示本轮可见功能与
|
||||
|
||||
Reference in New Issue
Block a user