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.
- {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/PKG-INFO +25 -10
- {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/README.md +23 -9
- {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/README.zh.md +19 -9
- {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/pyproject.toml +2 -1
- {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/src/nlalpha_data/__init__.py +1 -1
- {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/src/nlalpha_data/loader.py +1 -51
- {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/src/nlalpha_data/local_provider.py +1 -1
- {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/src/nlalpha_data/mock_provider.py +1 -1
- nlalpha_data-0.4.0/src/nlalpha_data/provider/__init__.py +63 -0
- nlalpha_data-0.4.0/src/nlalpha_data/provider/adjustment.py +52 -0
- nlalpha_data-0.4.0/src/nlalpha_data/provider/bars.py +83 -0
- nlalpha_data-0.4.0/src/nlalpha_data/provider/calendars.py +67 -0
- nlalpha_data-0.4.0/src/nlalpha_data/provider/errors.py +61 -0
- nlalpha_data-0.4.0/src/nlalpha_data/provider/manifest.py +72 -0
- nlalpha_data-0.3.0/src/nlalpha_data/base.py → nlalpha_data-0.4.0/src/nlalpha_data/provider/protocol.py +19 -7
- nlalpha_data-0.4.0/src/nlalpha_data/provider/testing.py +142 -0
- nlalpha_data-0.4.0/src/nlalpha_data/provider_tools.py +196 -0
- {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/src/nlalpha_data/server.py +27 -64
- {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/tests/test_loader.py +0 -64
- nlalpha_data-0.4.0/tests/test_provider_sdk.py +229 -0
- nlalpha_data-0.4.0/tests/test_provider_tools.py +212 -0
- {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/tests/test_server_artifacts.py +105 -68
- {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/.dockerignore +0 -0
- {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/.gitignore +0 -0
- {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/Dockerfile +0 -0
- {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/LICENSE +0 -0
- {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/src/nlalpha_data/bars_aligner.py +0 -0
- {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/src/nlalpha_data/calendar/__init__.py +0 -0
- {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/src/nlalpha_data/calendar/cn_equity.py +0 -0
- {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/src/nlalpha_data/calendar/futures.py +0 -0
- {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/src/nlalpha_data/calendar/us_equity.py +0 -0
- {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/src/nlalpha_data/calendar_base.py +0 -0
- {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/src/nlalpha_data/calendar_tools.py +0 -0
- {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/src/nlalpha_data/discovery.py +0 -0
- {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/src/nlalpha_data/knowledge/loading.md +0 -0
- {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/src/nlalpha_data/knowledge/local-provider.md +0 -0
- {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/src/nlalpha_data/parquet_store.py +0 -0
- {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/src/nlalpha_data/sqlite_store.py +0 -0
- {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/src/nlalpha_data/store.py +0 -0
- {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/tests/test_cache_contract.py +0 -0
- {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/tests/test_confess_contract.py +0 -0
- {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/tests/test_discovery.py +0 -0
- {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/tests/test_failure_envelope.py +0 -0
- {nlalpha_data-0.3.0 → nlalpha_data-0.4.0}/tests/test_mock_provider.py +0 -0
- {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
|
+
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.
|
|
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
|
-
| `
|
|
49
|
-
| `
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
| `
|
|
25
|
-
| `
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
| `
|
|
25
|
-
| `
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
39
|
+
from nlalpha_data.provider.protocol import _interval_delta
|
|
40
40
|
from nlalpha_data.store import filter_bar_dates
|
|
41
41
|
|
|
42
42
|
|
|
@@ -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
|
+
]
|