mirror of
https://github.com/ZhuLinsen/daily_stock_analysis
synced 2026-09-20 10:53:33 +08:00
fix: support registered index calendar identities
This commit is contained in:
@@ -25,7 +25,7 @@
|
||||
|
||||
这些接口继承现有 `/api/v1/*` 管理员会话认证:`ADMIN_AUTH_ENABLED=true` 时必须携带有效的管理员 Cookie,并可能返回 `401`;关闭认证时不强制 Cookie。本功能不新增独立认证方式。
|
||||
|
||||
列表查询的日期范围首尾均包含。未传日期时,默认返回从服务器当天开始的 7 个自然日(当天到 `today + 6 days`);若起始日接近 `9999-12-31`,结束日钳制到 `date.max`,不会因默认窗口溢出而返回 500。`page` 范围为 1 到 1,000,000,超出范围在进入数据库前返回 4xx;repository 也拒绝超过数据库整数范围的 offset。Dashboard 可以不传 scope 获取混合事件;股票详情传 `symbol`,同一 API 会把省略的 `scope_type` 视为 `symbol`,并对同时出现的 `scope_value` 使用同一身份规则归一和校验,严格返回该股票事件,不在客户端重新拼装或猜测归属。写入与查询都会复用仓库统一股票身份规范化,例如裸五位港股 `00700`、`00700.HK` 与 `HK00700` 统一为 `HK00700`,`600519.SH` / `600519` 统一为 `600519`。symbol scope 的 market 由解析身份自动回填;显式 market 会作为裸数字身份的消歧义 hint,解析后若仍冲突或 symbol 无法解析则返回 400,自由文本标识应使用 custom scope。
|
||||
列表查询的日期范围首尾均包含。未传日期时,默认返回从服务器当天开始的 7 个自然日(当天到 `today + 6 days`);若起始日接近 `9999-12-31`,结束日钳制到 `date.max`,不会因默认窗口溢出而返回 500。`page` 范围为 1 到 1,000,000,超出范围在进入数据库前返回 4xx;repository 也拒绝超过数据库整数范围的 offset。Dashboard 可以不传 scope 获取混合事件;股票详情传 `symbol`,同一 API 会把省略的 `scope_type` 视为 `symbol`,并对同时出现的 `scope_value` 使用同一身份规则归一和校验,严格返回该标的事件,不在客户端重新拼装或猜测归属。写入与查询都会复用仓库统一身份规范化,例如裸五位港股 `00700`、`00700.HK` 与 `HK00700` 统一为 `HK00700`,`600519.SH` / `600519` 统一为 `600519`;已登记指数也走同一 symbol 边界,`sh000300` / `000300.SH` 统一为 `sh000300`,`csi930955` / `930955.CSI` 统一为 `csi930955`。symbol scope 的 market 由解析身份自动回填;显式 market 会作为裸数字股票身份的消歧义 hint,解析后若仍冲突或 symbol 无法解析则返回 400,自由文本标识应使用 custom scope。
|
||||
|
||||
`metadata` 必须是只包含有限 JSON 数值的对象;`NaN` / `Infinity` 等非有限值在写入前返回 400。历史脏数据若包含非有限 metadata,列表会降级为空对象,避免单条记录拖垮整个响应。`symbol` 只属于 `scope_type=symbol` 的可查询契约;portfolio、sector 和 custom 事件若携带 symbol 会返回 400,避免写入后无法通过股票详情查询。删除路径的 `event_id` 限制为数据库有符号 64 位正整数,越界请求在数据库查询前返回 422。
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from typing import Any, Dict, Optional
|
||||
from data_provider.base import canonical_stock_code
|
||||
from src.repositories.calendar_event_repo import CalendarEventRepository
|
||||
from src.services.stock_code_utils import resolve_daily_stock_identity
|
||||
from src.services.stock_list_parser import ParseStatus, parse_analysis_target
|
||||
|
||||
_ALLOWED_EVENT_TYPES = {"earnings", "dividend", "lockup_unlock", "macro", "user", "monitor"}
|
||||
_ALLOWED_SCOPE_TYPES = {"market", "symbol", "portfolio", "sector", "custom"}
|
||||
@@ -200,6 +201,11 @@ class CalendarEventService:
|
||||
normalized = CalendarEventService._optional_normalized(value)
|
||||
if normalized is None:
|
||||
return None, None
|
||||
analysis_target = parse_analysis_target(normalized)
|
||||
if analysis_target.asset_type == ParseStatus.INDEX:
|
||||
if market_hint and market_hint != "cn":
|
||||
raise CalendarEventServiceError("market conflicts with symbol identity")
|
||||
return analysis_target.canonical_id, "cn"
|
||||
identity = resolve_daily_stock_identity(normalized, market_hint=market_hint)
|
||||
if identity is None or identity.market not in _ALLOWED_MARKETS - {"global"}:
|
||||
raise CalendarEventServiceError("unsupported symbol identity")
|
||||
|
||||
@@ -192,6 +192,55 @@ class CalendarEventApiTestCase(unittest.TestCase):
|
||||
self.assertEqual(conflicting_scope_value.status_code, 400)
|
||||
self.assertEqual(conflicting_scope_value.json()["error"], "validation_error")
|
||||
|
||||
def test_registered_index_aliases_round_trip_through_symbol_contract(self) -> None:
|
||||
start = date.today()
|
||||
end = start + timedelta(days=10)
|
||||
broad_market = self._create(
|
||||
title="CSI 300 rebalance",
|
||||
market=None,
|
||||
symbol="000300.SH",
|
||||
event_date=(start + timedelta(days=1)).isoformat(),
|
||||
)
|
||||
csi = self._create(
|
||||
title="CSI thematic rebalance",
|
||||
market=None,
|
||||
symbol="930955.CSI",
|
||||
event_date=(start + timedelta(days=2)).isoformat(),
|
||||
)
|
||||
|
||||
self.assertEqual(broad_market["symbol"], "sh000300")
|
||||
self.assertEqual(broad_market["scope_value"], "sh000300")
|
||||
self.assertEqual(broad_market["market"], "cn")
|
||||
self.assertEqual(csi["symbol"], "csi930955")
|
||||
self.assertEqual(csi["market"], "cn")
|
||||
|
||||
for alias, expected_id in (
|
||||
("sh000300", broad_market["id"]),
|
||||
("000300.SH", broad_market["id"]),
|
||||
("csi930955", csi["id"]),
|
||||
("930955.CSI", csi["id"]),
|
||||
):
|
||||
response = self.client.get(
|
||||
"/api/v1/calendar/events",
|
||||
params={
|
||||
"start_date": start.isoformat(),
|
||||
"end_date": end.isoformat(),
|
||||
"symbol": alias,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200, response.text)
|
||||
self.assertEqual([item["id"] for item in response.json()["items"]], [expected_id])
|
||||
|
||||
shanghai_50 = self._create(market=None, symbol="sh000016")
|
||||
self.assertEqual(shanghai_50["symbol"], "sh000016")
|
||||
self.assertEqual(
|
||||
self.client.get(
|
||||
"/api/v1/calendar/events",
|
||||
params={"symbol": "000016.SH"},
|
||||
).json()["items"][0]["id"],
|
||||
shanghai_50["id"],
|
||||
)
|
||||
|
||||
def test_symbol_scope_derives_market_and_rejects_conflicts(self) -> None:
|
||||
hk_event = self._create(symbol="HK00700", market=None)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user