nlalpha-data 0.3.0__py3-none-any.whl

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/store.py ADDED
@@ -0,0 +1,316 @@
1
+ """Bar data storage abstraction layer.
2
+
3
+ BaseBarStore provides a unified interface for persisting and retrieving
4
+ Bar data, regardless of the underlying storage backend (parquet, SQLite,
5
+ PostgreSQL, ClickHouse, etc.).
6
+
7
+ The storage layer is part of the core infrastructure, while specific
8
+ backends can be plugged in via assembly profile or environment variables.
9
+
10
+ Example:
11
+ store = ParquetBarStore(cache_dir="workspace/cache")
12
+ await store.save(bars)
13
+ bars = await store.load("AAPL", "1d", "2023-01-01", "2023-12-31")
14
+ """
15
+
16
+ from abc import ABC, abstractmethod
17
+ from dataclasses import dataclass
18
+ from datetime import date, datetime, timedelta, timezone
19
+ from pathlib import Path
20
+ import json
21
+ from hashlib import sha256
22
+ import math
23
+ import re
24
+ from numbers import Real
25
+
26
+ from nlalpha.core.contracts.market import Bar
27
+ from nlalpha.core.contracts.market_source import (
28
+ BAR_CONTRACT, BAR_CONTRACT_KEY, BAR_CONTRACT_VERSION, SOURCE_KEY, read_source_contract,
29
+ )
30
+ from nlalpha.core.contracts.time import get_timezone, normalize_utc, timezone_metadata
31
+ from nlalpha.core.errors import DataError
32
+
33
+ BAR_CACHE_SCHEMA_VERSION = BAR_CONTRACT_VERSION
34
+ BAR_CACHE_NAMESPACE = f"bar-contract-v{BAR_CACHE_SCHEMA_VERSION}"
35
+
36
+
37
+ @dataclass
38
+ class BarOverview:
39
+ """Overview of bar data stored in a specific store.
40
+
41
+ Mirrors vnpy's BarOverview concept but uses our internal naming.
42
+ """
43
+
44
+ symbol: str
45
+ interval: str
46
+ count: int = 0
47
+ start: datetime | None = None
48
+ end: datetime | None = None
49
+
50
+
51
+ class BaseBarStore(ABC):
52
+ """Abstract bar data storage backend.
53
+
54
+ All storage implementations (Parquet, SQLite, PostgreSQL, etc.)
55
+ must inherit from this class and implement the abstract methods.
56
+
57
+ Design principles:
58
+ - save() is idempotent-ish: writing the same bars twice should not
59
+ corrupt data (backend-specific deduplication recommended).
60
+ - load() must return bars sorted ascending by datetime.
61
+ - load() filters by the exact [start, end] date range.
62
+ """
63
+
64
+ @abstractmethod
65
+ async def save(
66
+ self,
67
+ bars: list[Bar],
68
+ symbol: str = "",
69
+ interval: str = "",
70
+ start: str = "",
71
+ end: str = "",
72
+ ) -> int:
73
+ """Persist a list of Bar objects.
74
+
75
+ Args:
76
+ bars: List of Bar instances to save.
77
+ symbol: Instrument code (used as storage key).
78
+ interval: Bar interval (used as storage key).
79
+ start: Requested start date (used as storage key).
80
+ end: Requested end date (used as storage key).
81
+
82
+ Returns:
83
+ Number of bars actually persisted (after deduplication).
84
+ """
85
+ ...
86
+
87
+ @abstractmethod
88
+ async def load(
89
+ self,
90
+ symbol: str,
91
+ interval: str,
92
+ start: str,
93
+ end: str,
94
+ ) -> list[Bar]:
95
+ """Load bars for a given symbol, interval, and date range.
96
+
97
+ Args:
98
+ symbol: Instrument code, e.g. "AAPL", "000300.SH"
99
+ interval: Bar interval, e.g. "1d", "1m"
100
+ start: Start date, "YYYY-MM-DD"
101
+ end: End date, "YYYY-MM-DD"
102
+
103
+ Returns:
104
+ Bars sorted ascending by datetime. Empty list if no data.
105
+ """
106
+ ...
107
+
108
+ @abstractmethod
109
+ async def delete(self, symbol: str, interval: str) -> int:
110
+ """Delete all bars for a given symbol + interval.
111
+
112
+ Returns:
113
+ Number of bars deleted.
114
+ """
115
+ ...
116
+
117
+ @abstractmethod
118
+ async def get_overview(self) -> list[BarOverview]:
119
+ """Return an overview of all stored bar series.
120
+
121
+ Returns:
122
+ List of BarOverview, one per (symbol, interval) pair.
123
+ """
124
+ ...
125
+
126
+ def is_available(self) -> bool:
127
+ """Check if the storage backend is ready.
128
+
129
+ Defaults to True. Subclasses may override to check DB connectivity,
130
+ disk space, permissions, etc.
131
+ """
132
+ return True
133
+
134
+
135
+ # ---------------------------------------------------------------------------
136
+ # Shared parquet helpers (used by ParquetBarStore and available for reuse)
137
+ # ---------------------------------------------------------------------------
138
+
139
+ QUERY_KEY = b"nlalpha.query.v1"
140
+
141
+
142
+ def _cache_path(symbol: str, interval: str, start: str, end: str, cache_dir: str | Path) -> Path:
143
+ """Hash the exact query into a collision-resistant safe filename."""
144
+ query = json.dumps([symbol, interval, start, end], separators=(",", ":"), ensure_ascii=False)
145
+ return Path(cache_dir) / f"{sha256(query.encode()).hexdigest()}.parquet"
146
+
147
+
148
+ def _bars_to_df(bars: list[Bar]):
149
+ """Return the canonical UTC bar frame with original source metadata."""
150
+ return _bars_to_table(bars).to_pandas()
151
+
152
+
153
+ def _df_to_bars(df, fallback_symbol: str = "") -> list[Bar]:
154
+ """Restore canonical bars without losing source metadata or precision."""
155
+ import pandas as pd
156
+ missing = set(_BAR_COLUMNS) - set(df.columns)
157
+ if missing:
158
+ raise DataError(
159
+ f"Legacy or incomplete bar cache lacks {sorted(missing)}; recreate it or import with an explicit source time contract.",
160
+ details={"time_contract": True},
161
+ )
162
+ bars: list[Bar] = []
163
+ for row in df.to_dict("records"):
164
+ def optional(name):
165
+ value = row[name]
166
+ return None if pd.isna(value) else value
167
+ session = optional("session_date")
168
+ if isinstance(session, datetime):
169
+ session = session.date()
170
+ elif isinstance(session, str):
171
+ session = date.fromisoformat(session)
172
+ for name in ("source_timezone", "calendar_id"):
173
+ value = optional(name)
174
+ if value is not None and (not isinstance(value, str) or not value or value != value.strip()):
175
+ raise DataError(f"Invalid canonical source metadata {name}.", details={"time_contract": True})
176
+ semantics = row["source_timestamp_semantics"]
177
+ if not isinstance(semantics, str) or semantics not in {"open", "close", "trading_date"}:
178
+ raise DataError("Invalid canonical source timestamp semantics.", details={"time_contract": True})
179
+ offset = optional("source_utc_offset_seconds")
180
+ if offset is not None and (isinstance(offset, bool) or not isinstance(offset, Real) or not math.isfinite(offset) or offset != int(offset)):
181
+ raise DataError("Source UTC offset must be exact integer seconds.", details={"time_contract": True})
182
+ bars.append(Bar(
183
+ symbol=row["symbol"] or fallback_symbol,
184
+ datetime=_canonical_datetime(row["close_time"]),
185
+ open_time=_canonical_datetime(optional("open_time")) if optional("open_time") is not None else None,
186
+ open=float(row["open"]), high=float(row["high"]), low=float(row["low"]),
187
+ close=float(row["close"]), volume=float(row["volume"]),
188
+ open_interest=float(optional("open_interest")) if optional("open_interest") is not None else None,
189
+ session_date=session,
190
+ source_timezone=optional("source_timezone"),
191
+ source_utc_offset_seconds=int(offset) if offset is not None else None,
192
+ source_timestamp_semantics=semantics,
193
+ calendar_id=optional("calendar_id"),
194
+ ))
195
+ bars.sort(key=lambda b: b.datetime)
196
+ return bars
197
+
198
+
199
+ _BAR_COLUMNS = (
200
+ "symbol", "open_time", "close_time", "open", "high", "low", "close", "volume",
201
+ "open_interest", "session_date", "source_timezone", "source_utc_offset_seconds",
202
+ "source_timestamp_semantics", "calendar_id",
203
+ )
204
+ TIME_CONTRACT_KEY = b"nlalpha.time_contract"
205
+
206
+
207
+ def _canonical_datetime(value) -> datetime:
208
+ """Require an aware timestamp representable by the Bar transport."""
209
+ if getattr(value, "nanosecond", 0):
210
+ raise DataError("Bar transport cannot truncate sub-microsecond timestamps.", details={"time_contract": True})
211
+ if hasattr(value, "to_pydatetime"):
212
+ value = value.to_pydatetime()
213
+ elif isinstance(value, str):
214
+ fractional = re.search(r"[T ]\d{2}:\d{2}:\d{2}\.(\d+)", value)
215
+ if fractional and any(character != "0" for character in fractional.group(1)[6:]):
216
+ raise DataError("Bar transport cannot truncate sub-microsecond timestamps.", details={"time_contract": True})
217
+ value = datetime.fromisoformat(value)
218
+ return normalize_utc(value)
219
+
220
+
221
+ def bar_time_contract(bars: list[Bar]) -> dict:
222
+ """Summarize declared source semantics for canonical UTC bar transport."""
223
+ from importlib.metadata import version
224
+ assets = {}
225
+ for bar in bars:
226
+ item = assets.setdefault(bar.symbol, {"source_timezones": set(), "source_timestamp_semantics": set(), "calendar_ids": set(), "session_date_available": True})
227
+ if bar.source_timezone is not None:
228
+ item["source_timezones"].add(bar.source_timezone)
229
+ item["source_timestamp_semantics"].add(bar.source_timestamp_semantics)
230
+ if bar.calendar_id is not None:
231
+ item["calendar_ids"].add(bar.calendar_id)
232
+ item["session_date_available"] &= bar.session_date is not None
233
+ for item in assets.values():
234
+ for name in ("source_timezones", "source_timestamp_semantics", "calendar_ids"):
235
+ item[name] = sorted(item[name])
236
+ return {
237
+ "schema": "nlalpha.market.time.v1", "timezone": "UTC",
238
+ "timestamp_semantics": "bounds", "timestamp_precision": "us", "assets": assets,
239
+ "timezone_database": {"tzdata_version": timezone_metadata("UTC")["tzdata_version"]},
240
+ "calendar_library_version": version("exchange-calendars"),
241
+ }
242
+
243
+
244
+ def _bars_to_table(bars: list[Bar], *, require_bounds: bool = False):
245
+ """Serialize exact UTC microseconds and attach a source time contract."""
246
+ import pyarrow as pa
247
+ if require_bounds and any(bar.open_time is None for bar in bars):
248
+ raise DataError("Bar delivery requires resolved open_time and close_time; declare source endpoint semantics or session calendar.", details={"time_contract": True})
249
+ types = {
250
+ "symbol": pa.string(), "open_time": pa.timestamp("us", tz="UTC"),
251
+ "close_time": pa.timestamp("us", tz="UTC"), "open": pa.float64(),
252
+ "high": pa.float64(), "low": pa.float64(), "close": pa.float64(),
253
+ "volume": pa.float64(), "open_interest": pa.float64(), "session_date": pa.date32(),
254
+ "source_timezone": pa.string(), "source_utc_offset_seconds": pa.int32(),
255
+ "source_timestamp_semantics": pa.string(), "calendar_id": pa.string(),
256
+ }
257
+ arrays = {}
258
+ for name in _BAR_COLUMNS:
259
+ attribute = "datetime" if name == "close_time" else name
260
+ values = [getattr(bar, attribute) for bar in bars]
261
+ if name in {"open_time", "close_time"}:
262
+ values = [normalize_utc(value) if value is not None else None for value in values]
263
+ arrays[name] = pa.array(values, type=types[name])
264
+ table = pa.table(arrays)
265
+ return table.replace_schema_metadata({TIME_CONTRACT_KEY: json.dumps(bar_time_contract(bars), sort_keys=True).encode("utf-8")})
266
+
267
+
268
+ def _table_to_bars(table, fallback_symbol: str = "") -> list[Bar]:
269
+ """Validate a canonical cache envelope before reconstructing bars."""
270
+ raw = (table.schema.metadata or {}).get(TIME_CONTRACT_KEY)
271
+ if raw is None:
272
+ raise DataError("Legacy bar cache has no nlalpha.time_contract; recreate it or import with an explicit source time contract.", details={"time_contract": True})
273
+ try:
274
+ contract = json.loads(raw)
275
+ except (ValueError, TypeError) as exc:
276
+ raise DataError("Malformed bar cache time contract.", details={"time_contract": True}) from exc
277
+ for name in ("open_time", "close_time"):
278
+ field = table.schema.field(name) if name in table.column_names else None
279
+ if field is None or not hasattr(field.type, "tz") or field.type.tz != "UTC" or field.type.unit != "us":
280
+ raise DataError(f"Canonical cache {name} must be an aware UTC microsecond timestamp.", details={"time_contract": True})
281
+ try:
282
+ bars = _df_to_bars(table.to_pandas(), fallback_symbol)
283
+ except Exception as exc:
284
+ if isinstance(exc, DataError) and exc.details.get("time_contract"):
285
+ raise
286
+ raise DataError(f"Invalid canonical bar cache: {exc}", details={"time_contract": True}) from exc
287
+ if contract != bar_time_contract(bars):
288
+ raise DataError("Bar cache time envelope contradicts its source metadata.", details={"time_contract": True})
289
+ return bars
290
+
291
+
292
+ def bar_source_date(bar: Bar) -> date:
293
+ """Resolve the session label or the original timestamp's source civil date."""
294
+ if bar.session_date is not None:
295
+ return bar.session_date
296
+ if bar.source_timestamp_semantics == "open":
297
+ if bar.open_time is None:
298
+ raise DataError("Open timestamp semantics requires open_time for source-date filtering.", details={"time_contract": True})
299
+ instant = bar.open_time
300
+ else:
301
+ instant = bar.datetime
302
+ if bar.source_timezone is not None:
303
+ source_zone = get_timezone(bar.source_timezone)
304
+ elif bar.source_utc_offset_seconds is not None:
305
+ source_zone = timezone(timedelta(seconds=bar.source_utc_offset_seconds))
306
+ else:
307
+ raise DataError("Date filtering requires a session label or original source timezone/offset.", details={"time_contract": True})
308
+ return instant.astimezone(source_zone).date()
309
+
310
+
311
+ def filter_bar_dates(bars: list[Bar], start: str, end: str) -> list[Bar]:
312
+ """Filter inclusive dates using each bar's original source semantics."""
313
+ begin, finish = date.fromisoformat(start), date.fromisoformat(end)
314
+ if begin > finish:
315
+ raise DataError("Requested start date is after end date.")
316
+ return [bar for bar in bars if begin <= bar_source_date(bar) <= finish]
@@ -0,0 +1,149 @@
1
+ Metadata-Version: 2.5
2
+ Name: nlalpha-data
3
+ Version: 0.3.0
4
+ Summary: Financial data infrastructure for nlalpha: providers, stores, and trading calendars
5
+ Project-URL: Homepage, https://github.com/ruimarj/nlalpha-data
6
+ Project-URL: Repository, https://github.com/ruimarj/nlalpha-data
7
+ Project-URL: Issues, https://github.com/ruimarj/nlalpha-data/issues
8
+ Project-URL: Changelog, https://github.com/ruimarj/nlalpha-data/releases
9
+ License-Expression: AGPL-3.0-or-later
10
+ License-File: LICENSE
11
+ Requires-Python: <3.13,>=3.12
12
+ Requires-Dist: aiofiles>=24.1
13
+ Requires-Dist: aiosqlite>=0.19
14
+ Requires-Dist: exchange-calendars>=4.13
15
+ Requires-Dist: mcp<2.0,>=1.0
16
+ Requires-Dist: nlalpha-core[json-log,redis,telemetry]>=0.7.0
17
+ Requires-Dist: pandas<3,>=2.3
18
+ Requires-Dist: pyarrow>=10.0
19
+ Requires-Dist: starlette>=0.37
20
+ Requires-Dist: uvicorn>=0.30
21
+ Provides-Extra: s3
22
+ Requires-Dist: nlalpha-core[s3]>=0.7.0; extra == 's3'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # nlalpha-data 0.3.0
26
+
27
+ 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
+ ## Positioning
30
+
31
+ nlalpha-data runs as a standalone MCP server (SSE transport, port 8000) and serves as the **data source** of the nlalpha ecosystem:
32
+
33
+ - Publishes market data to `DataArtifactStore` through `load_bars` and returns a `data_ref` for nlalpha-backtest to read and run backtests.
34
+ - Provides data coverage checks and quality evaluation, helping strategy generation judge the usable data range.
35
+ - Provides trading calendar tools so strategy logic can reference trading-day information.
36
+
37
+ ## MCP tools
38
+
39
+ | Tool | Description |
40
+ |------|------|
41
+ | `list_data_providers` | Lists all discovered data providers and the default provider. |
42
+ | `check_data_coverage` | Checks data coverage for the specified symbols, range, and interval. |
43
+ | `get_data_quality` | Returns a data quality evaluation (missing rate, outliers, and so on). |
44
+ | `load_bars` | Loads bar data, publishes it as an artifact, and returns a `data_ref`. |
45
+ | `list_trading_calendars` | Lists the supported trading calendars. |
46
+ | `get_trading_days` | Queries the trading-day sequence of the specified calendar. |
47
+ | `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. |
54
+ | `confess` | Debugging: returns a summary of the service's own runtime configuration. |
55
+
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
+
58
+ ## Provider selection and price-source evidence
59
+
60
+ `load_bars`, `check_data_coverage`, and `get_data_quality` share the same `DataLoader` and can name a source explicitly through `provider_id`. When none is given, they use `NLALPHA_DATA_PROVIDER`, or auto-select when exactly one provider is available; a request fails when several providers are available and no default is set. The service never switches sources automatically between providers. `list_data_providers` lists only available entries, while `diagnose_providers` reports providers that are installed but unavailable or failed to load. `about` surfaces each provider's manifest (including the `capabilities` summary), and `confess(provider_id)` returns the manifest plus optional sections of provider guidance.
61
+
62
+ `price_adjustment` can request `unadjusted`, `forward`, or `backward`. When a request explicitly requires an attribute, the selected provider must declare the same attribute for that symbol before loading; unknown or mismatched attributes fail the request explicitly and are never bypassed by switching sources. When no price attribute is requested, the source response still records `unknown` faithfully. Tushare supports `unadjusted`, `forward`, and `backward` for stock and ETF/fund daily bars, while continuous futures daily bars support only `forward`/`backward`; the adjustment baseline and anchor are defined by the selected provider's documentation, and the core layer defines no vendor-specific rules. Tushare provides daily bars for stocks, indices, and futures as well as ETF/funds, convertible bonds, and options (the latter three daily only); see its provider guidance for instrument identification, volume units, and adjustment declarations. The local file provider reports price attributes as `unknown` by default; the host can supply a JSON object through `NLALPHA_LOCAL_PRICE_ADJUSTMENTS`, for example `{"600000.SH":"forward"}`. That value is a host declaration, and the service never infers adjustment state from the price series.
63
+
64
+ `source` in responses is bound to the provider actually used for loading, and contains `provider_id`, the distribution package name, the version, `price_adjustment`, evidence, and `cache_hit`. Parquet metadata stores the same source contract under `nlalpha.source.v1` with schema `nlalpha.market.source.v1`, and records the asset source per symbol. Cache directories are isolated by source fingerprint, and file names are generated from the exact query; writes use a temporary file in the same directory plus atomic replacement, with asynchronous reads and writes running on threads. A corrupted cache, or metadata that does not match the query or source, returns an explicit error and is never treated as a cache miss or silently overwritten.
65
+
66
+ ## DataArtifactStore and identity scope
67
+
68
+ `load_bars` publishes canonical Parquet bytes to the core `DataArtifactStore` and returns a `data_ref` along with the time contract. Each reference binds fixed content, schema, digest, size, creation and expiry times, and the request scope; repeated loads obtain a new reference and do not overwrite the old object. Storage access is held by the application instance created by `create_app()`, and `lifespan` starts and closes the store and the provider cache. `/health` indicates process liveness; `/ready` calls the health check of the selected backend after startup completes (Redis `PING` or S3 `HeadBucket`), and a failed check returns `503 not_ready`.
69
+
70
+ The storage backend is selected explicitly by `NLALPHA_ARTIFACT_BACKEND` and defaults to `redis`; `s3` uses an S3-compatible service, with MinIO optional. S3 does not depend on a Redis index. There is no default 10 MiB size limit; a payload is limited only when `NLALPHA_ARTIFACT_MAX_BYTES` is explicitly configured. Logical TTL is checked exactly per object; S3 bucket lifecycle physical cleanup runs asynchronously in whole days and is not guaranteed to delete in the same second as the logical expiry.
71
+
72
+ | Setting | Default | Purpose |
73
+ |---|---|---|
74
+ | `NLALPHA_ARTIFACT_BACKEND` | `redis` | `redis` or `s3` |
75
+ | `NLALPHA_ARTIFACT_REDIS_URL` | `redis://127.0.0.1:6379/0` | Redis artifact backend |
76
+ | `NLALPHA_ARTIFACT_TTL_SECONDS` | `86400` | Logical TTL of a single reference |
77
+ | `NLALPHA_ARTIFACT_MAX_BYTES` | not set | Optional payload byte limit |
78
+ | `NLALPHA_ARTIFACT_S3_BUCKET` | required for S3 | S3 bucket |
79
+ | `NLALPHA_ARTIFACT_S3_ENDPOINT` | not set | Optional S3/MinIO endpoint |
80
+ | `NLALPHA_ARTIFACT_S3_ACCESS_KEY`, `NLALPHA_ARTIFACT_S3_SECRET_KEY` | not set | Optional paired credentials |
81
+ | `NLALPHA_ARTIFACT_S3_REGION` | not set | Optional S3 region |
82
+ | `NLALPHA_ARTIFACT_S3_RETENTION_DAYS` | `1` | Bucket lifecycle retention in days; must cover the logical TTL |
83
+
84
+ Before startup, S3 checks the bucket, that versioning is disabled, and the presence of a lifecycle rule covering the dedicated `nlalpha/artifacts/v1/` prefix. Do not treat a lifecycle rule test as evidence that objects have actually been waited past expiry.
85
+
86
+ The identity scope is resolved by `HostIdentityConfig` into `(environment, owner)`, and `NLALPHA_ENV` defaults to `local` and participates in isolation. `NLALPHA_IDENTITY_MODE` defaults to `single-user`: the owner is `standalone`, and requests that carry identity headers are rejected. Shared hosts should use `trusted-host` and configure `NLALPHA_IDENTITY_USER_HEADER` (default `X-NLAlpha-User-ID`); that header must be written and protected by an authenticated trusted gateway. NLAlpha only maps host identity and does not authenticate users itself.
87
+
88
+ DNS rebinding protection stays enabled. Allowed hosts are configured explicitly through `create_app(allowed_hosts=...)` or supplied by `NLALPHA_ALLOWED_HOSTS`; when unset, only localhost/loopback is allowed. The `*` wildcard host is not accepted.
89
+
90
+ ## Dependencies
91
+
92
+ | Package | Relationship |
93
+ |----|------|
94
+ | `nlalpha-core` | DataArtifactStore (cross-service data handoff), telemetry (metrics/logs), platform (host identity mapping) |
95
+ | `exchange-calendars` | Underlying calendar library for the trading calendar tools (`get_trading_days` and others) |
96
+ | Data provider plugins | Discovered through entry points, such as local file, Parquet, and mock |
97
+
98
+ ## Running
99
+
100
+ ```bash
101
+ NLALPHA_ARTIFACT_BACKEND=redis \
102
+ NLALPHA_ARTIFACT_REDIS_URL=redis://127.0.0.1:6379/0 \
103
+ uvicorn nlalpha_data.server:app --host 127.0.0.1 --port 8000
104
+ ```
105
+
106
+ 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
+
108
+ ## Source time contract
109
+
110
+ `load_bars` returns a `time_contract` summary alongside the `data_ref`. Canonical Parquet uses UTC `open_time`/`close_time` bounds at microsecond Bar precision and `nlalpha.market.time.v1` metadata. Per-asset metadata retains original source timezones, source timestamp semantics, calendar IDs, and session-date availability. Consumers should use this declared contract rather than infer a zone or trading date from UTC values. Store/cache round trips preserve all Bar time fields; legacy caches fail with an actionable recreate/import error rather than being guessed or migrated destructively; Parquet files with naive timestamps or incomplete source contracts are rejected. Sub-microsecond Bar conversion is rejected.
111
+
112
+ The local provider accepts constructor `time_contracts` keyed by symbol or matching `<symbol>.time.json` sidecars (`timezone`, `timestamp_semantics`, optional `calendar_id`). Aware explicit bounds can be normalized without a source-zone guess. Naive timestamps need an explicit supported IANA timezone; a single timestamp column also needs `open` or `close` semantics and a valid interval. Date-only labels require a calendar ID and exact session bounds. Calendar lookup uses actual sessions and half-days for supported exchanges; unknown calendars fail. Futures need a symbol-specific accurate calendar supplied by the caller.
113
+
114
+ `load_bars` date filters are inclusive and prefer an actual `session_date`. If that label is absent, `source_timestamp_semantics="open"` selects the original `open_time` and `close` selects `close_time`; the selected instant is converted to its original source timezone or offset to obtain the local date. Filtering fails if neither a session label nor source timezone/offset is available, and never substitutes the UTC date.
115
+
116
+ ## Agent contract guidance
117
+
118
+ For Agent workflows, read the current data capability description before deciding which checks or data preparation are available. This README documents package behavior; the live capability description owns the callable contract. Report only coverage, quality, calendar, and time conclusions supported by the actual checks.
119
+
120
+ `load_bars` returns a `data_ref`, bar-count summary, and declared contracts; it does not return the bars or per-day observed session dates. A count cannot identify which sessions are missing. An exact missing-session count requires an authorized, currently callable artifact-reading capability that exposes the observed dates and an applicable instrument calendar; if either evidence source is unavailable, report the exact count as unknown.
121
+
122
+ The Arrow schema stores UTF-8 JSON time metadata under `nlalpha.time_contract` (`schema: nlalpha.market.time.v1`, canonical UTC bounds, precision, timezone database version, and per-asset source summaries). SQLite caches use exact integer nanosecond keys and separate open-time/metadata columns; legacy text-wallclock caches are rejected. A raw Bar may omit open time while stored internally, but a `load_bars` artifact must resolve both open and close bounds.
123
+
124
+ ## 0.2.1 bar contract
125
+
126
+ Loaded bars and exported refs declare `volume_unit` and `bar_contract`. The data service places that metadata into new artifacts; its tool response includes both fields. New SQLite cache identity is `bar_data_v2`; Parquet uses the `bar-contract-v2` namespace and schema metadata. Existing cache identities and refs are not overwritten or treated as the new contract. Reload old refs to obtain corrected unit metadata.
127
+
128
+ ## Focused verification
129
+
130
+ The parent-reviewed provider, Tushare, and backtest integration group passed 58 tests and 23 subtests, with 6 warnings, in 7.03 seconds. The run exercised the real local HTTP/Redis path with ordinary permissions. This is focused integration coverage, not a full workspace or release-gate result.
131
+
132
+ ## Canonical local provider guidance
133
+
134
+ Local CSV/Excel lookup, row and time metadata, host-declared price adjustment, availability, and cache identity are documented in [`local-provider.md`](src/nlalpha_data/knowledge/local-provider.md). An available directory does not establish that the requested symbol has a file or data. The provider does not substitute another source. A future `data.confess(provider_id="local")` consumer reads this guide from the provider package; runtime integration remains pending.
135
+
136
+
137
+ ## Description and release metadata
138
+
139
+ The data service exposes `about` and provider-level `confess` descriptions from its cached provider manifests, declared methods, resources, and actual runtime distribution metadata. Descriptions follow `nlalpha.description.v1`; full and selected-section responses carry the same `description_revision`. Provider selection, source evidence, readiness, and artifact scope above describe runtime behavior; merely having an installed provider does not mean it is configured or available.
140
+
141
+ The 64 KiB limit applies to the artifact metadata protocol, not payload data. Payload limits are opt-in through `NLALPHA_ARTIFACT_MAX_BYTES`; with no configured limit there is no package-level 10 MiB cap. Concurrent materialization and deserialized object memory remain deployment budgeting responsibilities.
142
+
143
+ ## License
144
+
145
+ `nlalpha-data` is a community distribution under AGPL-3.0-or-later. This license scope does not include the platform integration, the adapter, private enhancement packages, or third-party dependencies.
146
+
147
+ ## Container images
148
+
149
+ The public Redis image installs the non-editable data-service wheel closure on Python 3.12 slim. The S3 image is a distinct build using `ARTIFACT_BACKEND=s3`; it adds the S3 client dependency while MinIO remains an external integration-test service. Platform integration images are assembled separately from the platform root and enhanced lock.
@@ -0,0 +1,24 @@
1
+ nlalpha_data/__init__.py,sha256=tXqAWCWoz5e7I5HVb_79Iw1Ag8fIM4tQUjjATYiT8lE,1071
2
+ nlalpha_data/bars_aligner.py,sha256=eWk_6BnkqmgSDjxR-yvASw4EI9qTZVDiEM-3Grb4Mmk,2832
3
+ nlalpha_data/base.py,sha256=xkhEGGkvO_TaJpSc1SxhmvDViapBwFNKq31sY8PANhY,3284
4
+ nlalpha_data/calendar_base.py,sha256=undkziW1qDq__0_PUvt8GQSWrXsoVtZiMQqrPmpNRbw,7241
5
+ nlalpha_data/calendar_tools.py,sha256=8PE5C4oeqJ9YZBR9z-T_ryyf0789YHV7SKvgMrxpv1g,5314
6
+ nlalpha_data/discovery.py,sha256=7afazHho14w8I0285ntQeyOwOhsci2xLwmiYE5TB44o,6929
7
+ nlalpha_data/loader.py,sha256=27B2g7PIHVhkopIxjuKUiNr_GLacvWC_eedlfnLy1FM,10869
8
+ nlalpha_data/local_provider.py,sha256=Kc6MwEf3Vff2Tr9YZTTF9xJKzIYyG5EhU__EcUMlu1o,19885
9
+ nlalpha_data/mock_provider.py,sha256=-Yl32zhVKagB0NVz0hyJ0R776LkonY3TFZMe7hnlm_I,5868
10
+ nlalpha_data/parquet_store.py,sha256=Pp1gzWvxgB3R6oFQ0kdgvgvp7e1O9ns_U7Zby807118,6577
11
+ nlalpha_data/server.py,sha256=c_n-uTKbYXknhiCUa45hh79bxwummz8DizEJ3GkHtIY,20093
12
+ nlalpha_data/sqlite_store.py,sha256=7Wms-WlPLRl4acqeZpXrN9vMPnKhuFPlvM9EH4OFBn8,8480
13
+ nlalpha_data/store.py,sha256=4uBHuCo9p788md2KWaMP24bufqXGno7ylIoeWztj0JU,13643
14
+ nlalpha_data/calendar/__init__.py,sha256=ENn5QWGvh8VtU-xjtSe99n7Di4oaDHJzX9U_--Zavw4,437
15
+ nlalpha_data/calendar/cn_equity.py,sha256=LfEK4Tk4y-SxtdkcSqkt2RUycId9Rlvj8SfZZWZi0p0,1143
16
+ nlalpha_data/calendar/futures.py,sha256=slK8Q2-jHQp5DBT9g3Je_M8KFXjD0x4nbc9FvAvjnyU,1557
17
+ nlalpha_data/calendar/us_equity.py,sha256=0sYfoGO_iWRyga7DhIeOPATsmIi3mCC7o8__1xk-6KQ,856
18
+ nlalpha_data/knowledge/loading.md,sha256=2iFlRwHH8-o_f23s62hj_M0A-FoyQz3ZX7tVoldBwlE,4017
19
+ nlalpha_data/knowledge/local-provider.md,sha256=3kFFtlQrZva2NKUVIjYmlPLeEJ-X2SmW4ROfy0vModk,2696
20
+ nlalpha_data-0.3.0.dist-info/METADATA,sha256=B64bHQRu2nCxo2EkgVdyg9MD1nZX86qkmVBr0lZrdlE,15888
21
+ nlalpha_data-0.3.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
22
+ nlalpha_data-0.3.0.dist-info/entry_points.txt,sha256=hW4qBROeaBiWA-A-xuH1ZUnPBBzEYO44Qw0kCE26q7I,58
23
+ nlalpha_data-0.3.0.dist-info/licenses/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
24
+ nlalpha_data-0.3.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ nlalpha-data = nlalpha_data.server:main