nlalpha-data 0.3.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/PKG-INFO +25 -10
  2. {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/README.md +23 -9
  3. {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/README.zh.md +19 -9
  4. {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/pyproject.toml +2 -1
  5. {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/src/nlalpha_data/__init__.py +1 -1
  6. {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/src/nlalpha_data/loader.py +1 -51
  7. {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/src/nlalpha_data/local_provider.py +1 -1
  8. {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/src/nlalpha_data/mock_provider.py +1 -1
  9. nlalpha_data-0.4.0/src/nlalpha_data/provider/__init__.py +63 -0
  10. nlalpha_data-0.4.0/src/nlalpha_data/provider/adjustment.py +52 -0
  11. nlalpha_data-0.4.0/src/nlalpha_data/provider/bars.py +83 -0
  12. nlalpha_data-0.4.0/src/nlalpha_data/provider/calendars.py +67 -0
  13. nlalpha_data-0.4.0/src/nlalpha_data/provider/errors.py +61 -0
  14. nlalpha_data-0.4.0/src/nlalpha_data/provider/manifest.py +72 -0
  15. nlalpha_data-0.3.0/src/nlalpha_data/base.py → nlalpha_data-0.4.0/src/nlalpha_data/provider/protocol.py +19 -7
  16. nlalpha_data-0.4.0/src/nlalpha_data/provider/testing.py +142 -0
  17. nlalpha_data-0.4.0/src/nlalpha_data/provider_tools.py +196 -0
  18. {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/src/nlalpha_data/server.py +27 -64
  19. {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/tests/test_loader.py +0 -64
  20. nlalpha_data-0.4.0/tests/test_provider_sdk.py +229 -0
  21. nlalpha_data-0.4.0/tests/test_provider_tools.py +212 -0
  22. {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/tests/test_server_artifacts.py +105 -68
  23. {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/.dockerignore +0 -0
  24. {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/.gitignore +0 -0
  25. {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/Dockerfile +0 -0
  26. {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/LICENSE +0 -0
  27. {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/src/nlalpha_data/bars_aligner.py +0 -0
  28. {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/src/nlalpha_data/calendar/__init__.py +0 -0
  29. {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/src/nlalpha_data/calendar/cn_equity.py +0 -0
  30. {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/src/nlalpha_data/calendar/futures.py +0 -0
  31. {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/src/nlalpha_data/calendar/us_equity.py +0 -0
  32. {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/src/nlalpha_data/calendar_base.py +0 -0
  33. {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/src/nlalpha_data/calendar_tools.py +0 -0
  34. {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/src/nlalpha_data/discovery.py +0 -0
  35. {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/src/nlalpha_data/knowledge/loading.md +0 -0
  36. {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/src/nlalpha_data/knowledge/local-provider.md +0 -0
  37. {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/src/nlalpha_data/parquet_store.py +0 -0
  38. {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/src/nlalpha_data/sqlite_store.py +0 -0
  39. {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/src/nlalpha_data/store.py +0 -0
  40. {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/tests/test_cache_contract.py +0 -0
  41. {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/tests/test_confess_contract.py +0 -0
  42. {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/tests/test_discovery.py +0 -0
  43. {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/tests/test_failure_envelope.py +0 -0
  44. {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/tests/test_mock_provider.py +0 -0
  45. {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/tests/test_self_description.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: nlalpha-data
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Financial data infrastructure for nlalpha: providers, stores, and trading calendars
5
5
  Project-URL: Homepage, https://github.com/ruimarj/nlalpha-data
6
6
  Project-URL: Repository, https://github.com/ruimarj/nlalpha-data
@@ -12,6 +12,7 @@ Requires-Python: <3.13,>=3.12
12
12
  Requires-Dist: aiofiles>=24.1
13
13
  Requires-Dist: aiosqlite>=0.19
14
14
  Requires-Dist: exchange-calendars>=4.13
15
+ Requires-Dist: jsonschema<5,>=4.23
15
16
  Requires-Dist: mcp<2.0,>=1.0
16
17
  Requires-Dist: nlalpha-core[json-log,redis,telemetry]>=0.7.0
17
18
  Requires-Dist: pandas<3,>=2.3
@@ -22,7 +23,7 @@ Provides-Extra: s3
22
23
  Requires-Dist: nlalpha-core[s3]>=0.7.0; extra == 's3'
23
24
  Description-Content-Type: text/markdown
24
25
 
25
- # nlalpha-data 0.3.0
26
+ # nlalpha-data 0.4.0
26
27
 
27
28
  Data-access MCP server: provides market data access, coverage checks, quality evaluation, and trading calendar capabilities for strategy generation and backtesting, and publishes immutable data to other nlalpha services through the core `DataArtifactStore`.
28
29
 
@@ -34,6 +35,10 @@ nlalpha-data runs as a standalone MCP server (SSE transport, port 8000) and serv
34
35
  - Provides data coverage checks and quality evaluation, helping strategy generation judge the usable data range.
35
36
  - Provides trading calendar tools so strategy logic can reference trading-day information.
36
37
 
38
+ ## Provider SDK
39
+
40
+ `nlalpha_data.provider` is the provider-neutral SDK used to implement and test data sources. It exposes `protocol` (the `DataProvider` contract), `manifest` (schema validation), `adjustment` (price-adjustment declarations), `bars` (time-contract bar construction; `build_bar` accepts `source_utc_offset_seconds` for exact source-offset evidence), `calendars` (exchange-calendar helpers, including the `TradingCalendar` re-export), `errors` (normalized provider errors), and `testing` (a synthetic provider plus contract assertions). It contains no source-specific rules.
41
+
37
42
  ## MCP tools
38
43
 
39
44
  | Tool | Description |
@@ -45,15 +50,23 @@ nlalpha-data runs as a standalone MCP server (SSE transport, port 8000) and serv
45
50
  | `list_trading_calendars` | Lists the supported trading calendars. |
46
51
  | `get_trading_days` | Queries the trading-day sequence of the specified calendar. |
47
52
  | `is_trading_day` | Determines whether a date is a trading day. |
48
- | `list_instruments` | Lists instruments by asset class (stock/index/fund/future/cb/option). |
49
- | `get_trading_calendar` | Reads the normalized exchange calendar (including session open/close and the previous trading day) from the selected data source. |
50
- | `get_daily_basic` | Reads daily basic metrics for the specified instruments. |
51
- | `get_price_limits` | Reads daily price-limit prices for the specified instruments. |
52
- | `get_suspensions` | Reads daily suspension and resumption records for the specified instruments. |
53
- | `get_index_weights` | Reads constituent weights of the specified index. |
53
+ | `list_provider_tools` | Lists the tools one provider declares in its manifest. |
54
+ | `invoke_provider_tool` | Validates and invokes one manifest-declared provider tool with bounded output. |
54
55
  | `confess` | Debugging: returns a summary of the service's own runtime configuration. |
55
56
 
56
- `list_instruments`, `get_trading_calendar`, `get_daily_basic`, `get_price_limits`, `get_suspensions`, and `get_index_weights` reuse the `DataLoader` provider selection rules, and `source` in their responses contains `provider_id`, the distribution package name, and the version. When the selected provider does not implement the corresponding method, the service returns an explicit `data_error`; it never silently switches sources or returns an empty result.
57
+ ## Breaking changes in 0.4.0
58
+
59
+ The six provider-specific MCP tools `list_instruments`, `get_trading_calendar`, `get_daily_basic`, `get_price_limits`, `get_suspensions`, and `get_index_weights` were removed from the MCP tool surface. Their behavior now lives behind provider manifests: discover the tools with `list_provider_tools(provider_id)` and invoke them with `invoke_provider_tool(provider_id, tool_name, arguments)`. There is no compatibility alias — clients that called the old tool names must migrate to the gateway. A Tushare migration looks like:
60
+
61
+ ```python
62
+ await data.invoke_provider_tool("tushare", "get_daily_basic", {"symbol": "600000.SH", "start": "20240101", "end": "20240331"})
63
+ ```
64
+
65
+ ## Provider tool gateway
66
+
67
+ The data service exposes only manifest-declared provider tools through a generic gateway; it contains no vendor tool names and no method reflection. `list_provider_tools(provider_id)` returns the declared tools, descriptions, JSON Schemas, and provider identity. `invoke_provider_tool(provider_id, tool_name, arguments)` requires the tool to appear in that provider's manifest, validates `arguments` against the tool's `input_schema` with JSON Schema, then calls the provider's optional `invoke_tool`. Unknown tools and providers without the invocation capability return `unsupported_capability`; schema mismatches return `invalid_request`; timeouts and other invocation failures return `upstream_transient`. Provider errors already classified by the SDK propagate unchanged.
68
+
69
+ The response is always `{rows, source, truncated, returned_rows, total_rows}`. Rows are serialized as UTF-8 JSON and bounded by `NLALPHA_PROVIDER_TOOL_MAX_BYTES` (default `1048576`); an oversized result returns the largest fitting row prefix with `truncated=true` instead of an unbounded payload. Invocation is bounded by `NLALPHA_PROVIDER_TOOL_TIMEOUT_SECONDS` (default `30`, finite and positive). Invalid values for either variable fail service startup. `source` contains `provider_id`, the distribution package name, and its version. The gateway never switches sources automatically and never registers provider-specific MCP tools, so the MCP tool surface stays stable as providers are added.
57
70
 
58
71
  ## Provider selection and price-source evidence
59
72
 
@@ -100,9 +113,11 @@ DNS rebinding protection stays enabled. Allowed hosts are configured explicitly
100
113
  ```bash
101
114
  NLALPHA_ARTIFACT_BACKEND=redis \
102
115
  NLALPHA_ARTIFACT_REDIS_URL=redis://127.0.0.1:6379/0 \
103
- uvicorn nlalpha_data.server:app --host 127.0.0.1 --port 8000
116
+ nlalpha-data
104
117
  ```
105
118
 
119
+ `NLALPHA_HOST` and `NLALPHA_PORT` default to `127.0.0.1` and `8000`; `python -m nlalpha_data.server` is the equivalent module entry point.
120
+
106
121
  Production or container deployments should explicitly select a network-reachable Redis or S3 endpoint and set the allowed hosts. S3 deployments also require bucket lifecycle rules to be configured in advance; MinIO is only an optional S3 implementation.
107
122
 
108
123
  ## Source time contract
@@ -1,4 +1,4 @@
1
- # nlalpha-data 0.3.0
1
+ # nlalpha-data 0.4.0
2
2
 
3
3
  Data-access MCP server: provides market data access, coverage checks, quality evaluation, and trading calendar capabilities for strategy generation and backtesting, and publishes immutable data to other nlalpha services through the core `DataArtifactStore`.
4
4
 
@@ -10,6 +10,10 @@ nlalpha-data runs as a standalone MCP server (SSE transport, port 8000) and serv
10
10
  - Provides data coverage checks and quality evaluation, helping strategy generation judge the usable data range.
11
11
  - Provides trading calendar tools so strategy logic can reference trading-day information.
12
12
 
13
+ ## Provider SDK
14
+
15
+ `nlalpha_data.provider` is the provider-neutral SDK used to implement and test data sources. It exposes `protocol` (the `DataProvider` contract), `manifest` (schema validation), `adjustment` (price-adjustment declarations), `bars` (time-contract bar construction; `build_bar` accepts `source_utc_offset_seconds` for exact source-offset evidence), `calendars` (exchange-calendar helpers, including the `TradingCalendar` re-export), `errors` (normalized provider errors), and `testing` (a synthetic provider plus contract assertions). It contains no source-specific rules.
16
+
13
17
  ## MCP tools
14
18
 
15
19
  | Tool | Description |
@@ -21,15 +25,23 @@ nlalpha-data runs as a standalone MCP server (SSE transport, port 8000) and serv
21
25
  | `list_trading_calendars` | Lists the supported trading calendars. |
22
26
  | `get_trading_days` | Queries the trading-day sequence of the specified calendar. |
23
27
  | `is_trading_day` | Determines whether a date is a trading day. |
24
- | `list_instruments` | Lists instruments by asset class (stock/index/fund/future/cb/option). |
25
- | `get_trading_calendar` | Reads the normalized exchange calendar (including session open/close and the previous trading day) from the selected data source. |
26
- | `get_daily_basic` | Reads daily basic metrics for the specified instruments. |
27
- | `get_price_limits` | Reads daily price-limit prices for the specified instruments. |
28
- | `get_suspensions` | Reads daily suspension and resumption records for the specified instruments. |
29
- | `get_index_weights` | Reads constituent weights of the specified index. |
28
+ | `list_provider_tools` | Lists the tools one provider declares in its manifest. |
29
+ | `invoke_provider_tool` | Validates and invokes one manifest-declared provider tool with bounded output. |
30
30
  | `confess` | Debugging: returns a summary of the service's own runtime configuration. |
31
31
 
32
- `list_instruments`, `get_trading_calendar`, `get_daily_basic`, `get_price_limits`, `get_suspensions`, and `get_index_weights` reuse the `DataLoader` provider selection rules, and `source` in their responses contains `provider_id`, the distribution package name, and the version. When the selected provider does not implement the corresponding method, the service returns an explicit `data_error`; it never silently switches sources or returns an empty result.
32
+ ## Breaking changes in 0.4.0
33
+
34
+ The six provider-specific MCP tools `list_instruments`, `get_trading_calendar`, `get_daily_basic`, `get_price_limits`, `get_suspensions`, and `get_index_weights` were removed from the MCP tool surface. Their behavior now lives behind provider manifests: discover the tools with `list_provider_tools(provider_id)` and invoke them with `invoke_provider_tool(provider_id, tool_name, arguments)`. There is no compatibility alias — clients that called the old tool names must migrate to the gateway. A Tushare migration looks like:
35
+
36
+ ```python
37
+ await data.invoke_provider_tool("tushare", "get_daily_basic", {"symbol": "600000.SH", "start": "20240101", "end": "20240331"})
38
+ ```
39
+
40
+ ## Provider tool gateway
41
+
42
+ The data service exposes only manifest-declared provider tools through a generic gateway; it contains no vendor tool names and no method reflection. `list_provider_tools(provider_id)` returns the declared tools, descriptions, JSON Schemas, and provider identity. `invoke_provider_tool(provider_id, tool_name, arguments)` requires the tool to appear in that provider's manifest, validates `arguments` against the tool's `input_schema` with JSON Schema, then calls the provider's optional `invoke_tool`. Unknown tools and providers without the invocation capability return `unsupported_capability`; schema mismatches return `invalid_request`; timeouts and other invocation failures return `upstream_transient`. Provider errors already classified by the SDK propagate unchanged.
43
+
44
+ The response is always `{rows, source, truncated, returned_rows, total_rows}`. Rows are serialized as UTF-8 JSON and bounded by `NLALPHA_PROVIDER_TOOL_MAX_BYTES` (default `1048576`); an oversized result returns the largest fitting row prefix with `truncated=true` instead of an unbounded payload. Invocation is bounded by `NLALPHA_PROVIDER_TOOL_TIMEOUT_SECONDS` (default `30`, finite and positive). Invalid values for either variable fail service startup. `source` contains `provider_id`, the distribution package name, and its version. The gateway never switches sources automatically and never registers provider-specific MCP tools, so the MCP tool surface stays stable as providers are added.
33
45
 
34
46
  ## Provider selection and price-source evidence
35
47
 
@@ -76,9 +88,11 @@ DNS rebinding protection stays enabled. Allowed hosts are configured explicitly
76
88
  ```bash
77
89
  NLALPHA_ARTIFACT_BACKEND=redis \
78
90
  NLALPHA_ARTIFACT_REDIS_URL=redis://127.0.0.1:6379/0 \
79
- uvicorn nlalpha_data.server:app --host 127.0.0.1 --port 8000
91
+ nlalpha-data
80
92
  ```
81
93
 
94
+ `NLALPHA_HOST` and `NLALPHA_PORT` default to `127.0.0.1` and `8000`; `python -m nlalpha_data.server` is the equivalent module entry point.
95
+
82
96
  Production or container deployments should explicitly select a network-reachable Redis or S3 endpoint and set the allowed hosts. S3 deployments also require bucket lifecycle rules to be configured in advance; MinIO is only an optional S3 implementation.
83
97
 
84
98
  ## Source time contract
@@ -1,4 +1,4 @@
1
- # nlalpha-data 0.3.0
1
+ # nlalpha-data 0.4.0
2
2
 
3
3
  数据接入 MCP Server:为策略生成与回测提供行情数据接入、覆盖检查、质量评估与交易日历能力,并通过 core `DataArtifactStore` 向其他 nlalpha 服务交付不可变数据。
4
4
 
@@ -21,15 +21,23 @@ nlalpha-data 以独立 MCP Server(SSE transport,端口 8000)运行,作
21
21
  | `list_trading_calendars` | 列出支持的交易日历 |
22
22
  | `get_trading_days` | 查询指定日历的交易日序列 |
23
23
  | `is_trading_day` | 判断某日期是否为交易日 |
24
- | `list_instruments` | 按资产类别列出标的列表(stock/index/fund/future/cb/option) |
25
- | `get_trading_calendar` | 从所选数据源读取规范化交易所日历(含开闭市与前一交易日) |
26
- | `get_daily_basic` | 读取指定标的的每日基础指标 |
27
- | `get_price_limits` | 读取指定标的的每日涨跌停价格 |
28
- | `get_suspensions` | 读取指定标的的每日停复牌记录 |
29
- | `get_index_weights` | 读取指定指数的成分权重 |
24
+ | `list_provider_tools` | 列出某个 provider 在 manifest 中声明的工具 |
25
+ | `invoke_provider_tool` | 校验并调用一个 manifest 声明的 provider 工具,输出有界 |
30
26
  | `confess` | 调试用:返回服务自身的运行配置摘要 |
31
27
 
32
- `list_instruments`、`get_trading_calendar`、`get_daily_basic`、`get_price_limits`、`get_suspensions` 和 `get_index_weights` 复用 `DataLoader` 的 provider 选择规则,返回值中的 `source` 含 `provider_id`、发行包名与版本;所选 provider 未实现对应方法时返回明确的 `data_error`,不会静默换源或返回空结果。
28
+ ## 0.4.0 破坏性变更
29
+
30
+ 六个厂商专用 MCP 工具 `list_instruments`、`get_trading_calendar`、`get_daily_basic`、`get_price_limits`、`get_suspensions`、`get_index_weights` 已从 MCP 工具面移除,其能力改由 provider manifest 声明:先用 `list_provider_tools(provider_id)` 发现工具,再用 `invoke_provider_tool(provider_id, tool_name, arguments)` 调用。不提供兼容别名,仍调用旧工具名的客户端必须迁移到网关。Tushare 的迁移示例如下:
31
+
32
+ ```python
33
+ await data.invoke_provider_tool("tushare", "get_daily_basic", {"symbol": "600000.SH", "start": "20240101", "end": "20240331"})
34
+ ```
35
+
36
+ ## Provider 工具网关
37
+
38
+ data 服务通过通用网关只暴露 manifest 声明的 provider 工具,不包含任何厂商工具名,也不做方法反射。`list_provider_tools(provider_id)` 返回声明的工具、说明、JSON Schema 与 provider 身份。`invoke_provider_tool(provider_id, tool_name, arguments)` 要求工具出现在该 provider 的 manifest 中,先用 JSON Schema 校验 `arguments` 与工具的 `input_schema`,再调用 provider 可选的 `invoke_tool`。未知工具、以及未实现调用能力的 provider 返回 `unsupported_capability`;schema 不匹配返回 `invalid_request`;超时与其他调用失败返回 `upstream_transient`。SDK 已分类的 provider 错误原样传播。
39
+
40
+ 响应固定为 `{rows, source, truncated, returned_rows, total_rows}`。rows 以 UTF-8 JSON 序列化,并受 `NLALPHA_PROVIDER_TOOL_MAX_BYTES`(默认 `1048576`)约束;超限时返回能放入预算的最大行前缀并标记 `truncated=true`,绝不返回无界数据。调用受 `NLALPHA_PROVIDER_TOOL_TIMEOUT_SECONDS`(默认 `30`,必须有限且为正)约束。任一变量取值非法都会让服务启动失败。`source` 含 `provider_id`、发行包名与版本。网关不会自动换源,也不会动态注册厂商 MCP 工具,因此新增 provider 时 MCP 工具面保持稳定。
33
41
 
34
42
  ## Provider 选择与价格来源证据
35
43
 
@@ -76,9 +84,11 @@ DNS rebinding 防护保持开启。允许的 Host 通过 `create_app(allowed_hos
76
84
  ```bash
77
85
  NLALPHA_ARTIFACT_BACKEND=redis \
78
86
  NLALPHA_ARTIFACT_REDIS_URL=redis://127.0.0.1:6379/0 \
79
- uvicorn nlalpha_data.server:app --host 127.0.0.1 --port 8000
87
+ nlalpha-data
80
88
  ```
81
89
 
90
+ `NLALPHA_HOST` 与 `NLALPHA_PORT` 默认 `127.0.0.1`/`8000`;等价的模块入口是 `python -m nlalpha_data.server`。
91
+
82
92
  生产或容器部署应显式选择网络可达的 Redis 或 S3 endpoint,并设置允许的 Host。S3 部署还需提前配置 bucket 生命周期规则;MinIO 只是可选的 S3 实现。
83
93
 
84
94
  ## Source time contract
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nlalpha-data"
7
- version = "0.3.0"
7
+ version = "0.4.0"
8
8
  description = "Financial data infrastructure for nlalpha: providers, stores, and trading calendars"
9
9
  readme = "README.md"
10
10
  license = "AGPL-3.0-or-later"
@@ -20,6 +20,7 @@ dependencies = [
20
20
  "pyarrow>=10.0",
21
21
  "mcp>=1.0,<2.0",
22
22
  "exchange-calendars>=4.13",
23
+ "jsonschema>=4.23,<5",
23
24
  ]
24
25
 
25
26
  [project.optional-dependencies]
@@ -1,6 +1,6 @@
1
1
  """Financial data providers, explicit loading, storage, and trading calendars."""
2
2
 
3
- from nlalpha_data.base import DataProvider
3
+ from nlalpha_data.provider.protocol import DataProvider
4
4
  from nlalpha_data.store import BaseBarStore, BarOverview
5
5
  from nlalpha_data.calendar_base import TradingCalendar, WeekendOnlyCalendar
6
6
  from nlalpha_data.calendar import CNEquityCalendar, USEquityCalendar, FuturesCalendar
@@ -8,7 +8,7 @@ from typing import Any
8
8
 
9
9
  from nlalpha.core.contracts.market import Bar
10
10
  from nlalpha.core.errors import DataError, InvalidRequestError, NlAlphaError, INVALID_REQUEST
11
- from nlalpha_data.base import DataProvider, validate_request_description
11
+ from nlalpha_data.provider.protocol import DataProvider, validate_request_description
12
12
  from nlalpha_data.parquet_store import ParquetBarStore
13
13
 
14
14
 
@@ -153,56 +153,6 @@ class DataLoader:
153
153
  await store.save(bars, symbol=symbol, interval=interval, start=start, end=end)
154
154
  return LoadedBars(bars=bars, cache_hit=False, **source)
155
155
 
156
- @staticmethod
157
- def _source(record: DataProviderRecord) -> dict[str, Any]:
158
- """Return the provider identity attached to pass-through reference data."""
159
- return {"provider_id": record.provider_id, "package": record.package, "version": record.version}
160
-
161
- def _provider_call(self, method_name: str) -> tuple[DataProviderRecord, Any]:
162
- """Resolve one required provider method or expose its absence."""
163
- record = self._select(None)
164
- method = getattr(record.provider, method_name, None)
165
- if not callable(method):
166
- raise DataError(f"Selected data provider does not expose {method_name}.")
167
- return record, method
168
-
169
- async def list_instruments(self, asset: str, *, market: str | None = None,
170
- status: str | None = None) -> tuple[list[dict], dict[str, Any]]:
171
- """List one asset class's instrument master with its source identity."""
172
- record, method = self._provider_call("list_instruments")
173
- rows = await method(asset, market=market, status=status)
174
- return rows, self._source(record)
175
-
176
- async def get_trading_calendar(self, exchange: str, start: str, end: str) -> tuple[list[dict], dict[str, Any]]:
177
- """Return one exchange calendar with its source identity."""
178
- record, method = self._provider_call("get_trading_calendar")
179
- rows = await method(exchange, start, end)
180
- return rows, self._source(record)
181
-
182
- async def get_daily_basic(self, symbol: str, start: str, end: str) -> tuple[list[dict], dict[str, Any]]:
183
- """Return daily fundamental metrics with their source identity."""
184
- record, method = self._provider_call("get_daily_basic")
185
- rows = await method(symbol, start, end)
186
- return rows, self._source(record)
187
-
188
- async def get_price_limits(self, symbol: str, start: str, end: str) -> tuple[list[dict], dict[str, Any]]:
189
- """Return daily price limits with their source identity."""
190
- record, method = self._provider_call("get_price_limits")
191
- rows = await method(symbol, start, end)
192
- return rows, self._source(record)
193
-
194
- async def get_suspensions(self, symbol: str, start: str, end: str) -> tuple[list[dict], dict[str, Any]]:
195
- """Return daily suspension rows with their source identity."""
196
- record, method = self._provider_call("get_suspensions")
197
- rows = await method(symbol, start, end)
198
- return rows, self._source(record)
199
-
200
- async def get_index_weights(self, index_code: str, start: str, end: str) -> tuple[list[dict], dict[str, Any]]:
201
- """Return index constituent weights with their source identity."""
202
- record, method = self._provider_call("get_index_weights")
203
- rows = await method(index_code, start, end)
204
- return rows, self._source(record)
205
-
206
156
  async def aclose(self) -> None:
207
157
  """Close all cache resources owned by this loader."""
208
158
  for store in self._stores.values():
@@ -36,7 +36,7 @@ from nlalpha.core.contracts.market import Bar
36
36
  from nlalpha.core.errors import DataError
37
37
  from nlalpha.core.contracts.time import get_timezone, normalize_utc, timezone_metadata
38
38
  from nlalpha_data.calendar_base import exchange_calendar, session_bounds
39
- from nlalpha_data.base import _interval_delta
39
+ from nlalpha_data.provider.protocol import _interval_delta
40
40
  from nlalpha_data.store import filter_bar_dates
41
41
 
42
42
 
@@ -8,7 +8,7 @@ import math
8
8
 
9
9
  from nlalpha.core.contracts.market import Bar
10
10
 
11
- from nlalpha_data.base import _interval_delta
11
+ from nlalpha_data.provider.protocol import _interval_delta
12
12
 
13
13
 
14
14
  class MockDataProvider:
@@ -0,0 +1,63 @@
1
+ """Provider-neutral SDK contracts, manifests, adjustments, bars, calendars, and errors."""
2
+
3
+ from nlalpha_data.provider.protocol import (
4
+ DataProvider,
5
+ ToolInvokingProvider,
6
+ _interval_delta,
7
+ validate_request_description,
8
+ )
9
+ from nlalpha_data.provider.manifest import MANIFEST_SCHEMA, validate_manifest
10
+ from nlalpha_data.provider.adjustment import (
11
+ ADJUSTMENT_MODES,
12
+ AdjustmentSpec,
13
+ describe_adjustment,
14
+ validate_adjustment,
15
+ )
16
+ from nlalpha_data.provider.bars import build_bar, session_close, validate_bar_time
17
+ from nlalpha_data.provider.calendars import (
18
+ CalendarResolver,
19
+ get_calendar,
20
+ session_bounds,
21
+ trading_days,
22
+ )
23
+ from nlalpha_data.provider.errors import (
24
+ CONTRACT_VIOLATION,
25
+ DATA_UNAVAILABLE,
26
+ INVALID_REQUEST,
27
+ RATE_LIMITED,
28
+ UNSUPPORTED_CAPABILITY,
29
+ UPSTREAM_TRANSIENT,
30
+ classify_http_status,
31
+ provider_error,
32
+ )
33
+ from nlalpha_data.provider.testing import SyntheticDataProvider, assert_provider_contract
34
+
35
+ __all__ = [
36
+ "DataProvider",
37
+ "ToolInvokingProvider",
38
+ "validate_request_description",
39
+ "_interval_delta",
40
+ "MANIFEST_SCHEMA",
41
+ "validate_manifest",
42
+ "ADJUSTMENT_MODES",
43
+ "AdjustmentSpec",
44
+ "validate_adjustment",
45
+ "describe_adjustment",
46
+ "session_close",
47
+ "build_bar",
48
+ "validate_bar_time",
49
+ "get_calendar",
50
+ "session_bounds",
51
+ "trading_days",
52
+ "CalendarResolver",
53
+ "DATA_UNAVAILABLE",
54
+ "RATE_LIMITED",
55
+ "UPSTREAM_TRANSIENT",
56
+ "UNSUPPORTED_CAPABILITY",
57
+ "INVALID_REQUEST",
58
+ "CONTRACT_VIOLATION",
59
+ "provider_error",
60
+ "classify_http_status",
61
+ "SyntheticDataProvider",
62
+ "assert_provider_contract",
63
+ ]
@@ -0,0 +1,52 @@
1
+ """Describe provider price-adjustment declarations without vendor rules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+
7
+ ADJUSTMENT_MODES = ("unadjusted", "forward", "backward")
8
+ _BASIS = ("price", "total_return")
9
+ _ANCHORS = {"unadjusted": None, "forward": "end", "backward": "start"}
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class AdjustmentSpec:
14
+ """Declare a price-adjustment mode, its anchor, basis, and evidence."""
15
+
16
+ mode: str
17
+ anchor: str | None
18
+ basis: str
19
+ evidence: dict = field(default_factory=dict)
20
+
21
+
22
+ def validate_adjustment(mode, *, anchor=None, basis="price", evidence=None) -> AdjustmentSpec:
23
+ """Validate an adjustment declaration and derive its canonical anchor."""
24
+ if not isinstance(mode, str) or mode not in ADJUSTMENT_MODES:
25
+ raise ValueError("adjustment mode must be one of: unadjusted, forward, backward")
26
+ expected_anchor = _ANCHORS[mode]
27
+ if anchor is not None and anchor != expected_anchor:
28
+ raise ValueError(
29
+ f"adjustment mode {mode!r} requires anchor {expected_anchor!r}"
30
+ )
31
+ if not isinstance(basis, str) or basis not in _BASIS:
32
+ raise ValueError("adjustment basis must be price or total_return")
33
+ if evidence is None:
34
+ evidence = {}
35
+ if not isinstance(evidence, dict):
36
+ raise ValueError("adjustment evidence must be an object")
37
+ return AdjustmentSpec(mode=mode, anchor=expected_anchor, basis=basis, evidence=dict(evidence))
38
+
39
+
40
+ def describe_adjustment(spec: AdjustmentSpec) -> dict:
41
+ """Render an adjustment spec in the legacy source-declaration shape."""
42
+ if not isinstance(spec, AdjustmentSpec):
43
+ raise ValueError("describe_adjustment requires an AdjustmentSpec")
44
+ return {"price_adjustment": spec.mode, "evidence": dict(spec.evidence)}
45
+
46
+
47
+ __all__ = [
48
+ "ADJUSTMENT_MODES",
49
+ "AdjustmentSpec",
50
+ "validate_adjustment",
51
+ "describe_adjustment",
52
+ ]
@@ -0,0 +1,83 @@
1
+ """Provider-neutral helpers for constructing and validating bars."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import date, datetime
6
+ from dataclasses import replace
7
+
8
+ from nlalpha.core.contracts.market import Bar
9
+ from nlalpha.core.errors import DataError
10
+ from nlalpha_data.calendar_base import session_bounds
11
+ from nlalpha_data.provider.protocol import _interval_delta
12
+
13
+
14
+ def session_close(calendar_id: str, session_date: date) -> datetime:
15
+ """Return the authoritative UTC close for one labeled session."""
16
+ return session_bounds(calendar_id, session_date)[1]
17
+
18
+
19
+ def build_bar(
20
+ symbol: str,
21
+ interval: str,
22
+ *,
23
+ open: float,
24
+ high: float,
25
+ low: float,
26
+ close: float,
27
+ volume: float,
28
+ open_time: datetime | None = None,
29
+ close_time: datetime | None = None,
30
+ session_date: date | None = None,
31
+ source_timezone: str | None = None,
32
+ source_timestamp_semantics: str = "close",
33
+ calendar_id: str | None = None,
34
+ open_interest: float | None = None,
35
+ source_utc_offset_seconds: int | None = None,
36
+ ) -> Bar:
37
+ """Construct a Bar with an explicit, validated time contract."""
38
+ _interval_delta(interval)
39
+ if close_time is None:
40
+ if session_date is None or calendar_id is None:
41
+ raise DataError(
42
+ "close_time is required unless session_date and calendar_id resolve it.",
43
+ details={"time_contract": True, "field": "close_time"},
44
+ )
45
+ close_time = session_close(calendar_id, session_date)
46
+ if not isinstance(close_time, datetime):
47
+ raise DataError(
48
+ "close_time must be a datetime.",
49
+ details={"time_contract": True, "field": "close_time"},
50
+ )
51
+ return Bar(
52
+ symbol=symbol,
53
+ datetime=close_time,
54
+ open=open,
55
+ high=high,
56
+ low=low,
57
+ close=close,
58
+ volume=volume,
59
+ open_interest=open_interest,
60
+ open_time=open_time,
61
+ session_date=session_date,
62
+ source_timezone=source_timezone,
63
+ source_utc_offset_seconds=source_utc_offset_seconds,
64
+ source_timestamp_semantics=source_timestamp_semantics,
65
+ calendar_id=calendar_id,
66
+ )
67
+
68
+
69
+ def validate_bar_time(bar: Bar) -> None:
70
+ """Re-run Bar time invariants and raise DataError on violation."""
71
+ if not isinstance(bar, Bar):
72
+ raise DataError(
73
+ "bar must be a Bar instance.",
74
+ details={"time_contract": True, "field": "bar"},
75
+ )
76
+ replace(bar)
77
+
78
+
79
+ __all__ = [
80
+ "session_close",
81
+ "build_bar",
82
+ "validate_bar_time",
83
+ ]
@@ -0,0 +1,67 @@
1
+ """Provider-neutral trading-calendar helpers and exchange mapping."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import date, datetime
6
+
7
+ from nlalpha.core.errors import DataError
8
+ from nlalpha_data.calendar_base import TradingCalendar
9
+ from nlalpha_data.calendar_base import exchange_calendar, session_bounds as _session_bounds
10
+
11
+
12
+ def get_calendar(calendar_id: str):
13
+ """Resolve an explicitly named exchange calendar."""
14
+ return exchange_calendar(calendar_id)
15
+
16
+
17
+ def session_bounds(calendar_id: str, session_date: date) -> tuple[datetime, datetime]:
18
+ """Return the UTC open and close bounds of one labeled session."""
19
+ return _session_bounds(calendar_id, session_date)
20
+
21
+
22
+ def trading_days(calendar_id: str, start, end) -> list[date]:
23
+ """Return the sessions between two inclusive date labels."""
24
+ from pandas import Timestamp
25
+
26
+ calendar = get_calendar(calendar_id)
27
+ try:
28
+ sessions = calendar.sessions_in_range(Timestamp(start), Timestamp(end))
29
+ except Exception as exc:
30
+ raise DataError(
31
+ f"Cannot resolve {calendar_id} sessions {start}..{end}: {exc}",
32
+ details={"time_contract": True},
33
+ ) from exc
34
+ return [session.date() for session in sessions]
35
+
36
+
37
+ class CalendarResolver:
38
+ """Map exchange codes to calendar identifiers through an explicit table."""
39
+
40
+ def __init__(self, mapping: dict[str, str]):
41
+ if not isinstance(mapping, dict):
42
+ raise ValueError("calendar mapping must be an object")
43
+ for code, calendar_id in mapping.items():
44
+ if not isinstance(code, str) or not code or code != code.strip():
45
+ raise ValueError("calendar mapping keys must be non-empty trimmed strings")
46
+ if not isinstance(calendar_id, str) or not calendar_id or calendar_id != calendar_id.strip():
47
+ raise ValueError("calendar mapping values must be non-empty trimmed strings")
48
+ self._mapping = dict(mapping)
49
+
50
+ def resolve(self, exchange_code: str) -> str:
51
+ """Resolve one exchange code or fail with the unsupported exchange."""
52
+ calendar_id = self._mapping.get(exchange_code)
53
+ if calendar_id is None:
54
+ raise DataError(
55
+ f"Unsupported exchange code: {exchange_code!r}.",
56
+ details={"unsupported_exchange": exchange_code},
57
+ )
58
+ return calendar_id
59
+
60
+
61
+ __all__ = [
62
+ "TradingCalendar",
63
+ "get_calendar",
64
+ "session_bounds",
65
+ "trading_days",
66
+ "CalendarResolver",
67
+ ]
@@ -0,0 +1,61 @@
1
+ """Provider error categories and normalized DataError construction."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from nlalpha.core.errors import DataError
6
+
7
+ DATA_UNAVAILABLE = "data_unavailable"
8
+ RATE_LIMITED = "rate_limited"
9
+ UPSTREAM_TRANSIENT = "upstream_transient"
10
+ UNSUPPORTED_CAPABILITY = "unsupported_capability"
11
+ INVALID_REQUEST = "invalid_request"
12
+ CONTRACT_VIOLATION = "contract_violation"
13
+
14
+ CATEGORIES = frozenset(
15
+ {
16
+ DATA_UNAVAILABLE,
17
+ RATE_LIMITED,
18
+ UPSTREAM_TRANSIENT,
19
+ UNSUPPORTED_CAPABILITY,
20
+ INVALID_REQUEST,
21
+ CONTRACT_VIOLATION,
22
+ }
23
+ )
24
+ _RETRYABLE = frozenset({RATE_LIMITED, UPSTREAM_TRANSIENT})
25
+
26
+
27
+ def provider_error(category: str, message: str, *, details: dict | None = None,
28
+ retryable: bool | None = None) -> DataError:
29
+ """Build a DataError tagged with a provider error category."""
30
+ if category not in CATEGORIES:
31
+ raise ValueError(f"unknown provider error category: {category!r}")
32
+ payload = dict(details or {})
33
+ payload["provider_error_category"] = category
34
+ if retryable is None:
35
+ retryable = category in _RETRYABLE
36
+ return DataError(message, details=payload, retryable=retryable)
37
+
38
+
39
+ def classify_http_status(status: int) -> str:
40
+ """Map an HTTP status to a provider error category."""
41
+ if isinstance(status, bool) or not isinstance(status, int):
42
+ raise ValueError("HTTP status must be an integer")
43
+ if status == 429:
44
+ return RATE_LIMITED
45
+ if 500 <= status <= 599:
46
+ return UPSTREAM_TRANSIENT
47
+ if 400 <= status <= 499:
48
+ return INVALID_REQUEST
49
+ raise ValueError(f"unsupported HTTP status for provider classification: {status}")
50
+
51
+
52
+ __all__ = [
53
+ "DATA_UNAVAILABLE",
54
+ "RATE_LIMITED",
55
+ "UPSTREAM_TRANSIENT",
56
+ "UNSUPPORTED_CAPABILITY",
57
+ "INVALID_REQUEST",
58
+ "CONTRACT_VIOLATION",
59
+ "provider_error",
60
+ "classify_http_status",
61
+ ]