diff --git a/README.md b/README.md
index 0b279769..7ec8aeb8 100644
--- a/README.md
+++ b/README.md
@@ -128,7 +128,11 @@ fred = FREDProvider().fetch_series("GDP", "2020-01-01", "2024-12-31")
| Massive | US equities, options, futures, forex, crypto |
| Finnhub | 70+ global exchanges |
| OANDA | Forex broker data |
-| CryptoCompare | Crypto market data; free account key required |
+| CryptoCompare | Included adapter; not release-qualified for 0.1.0 |
+
+CryptoCompare account registration was unavailable during the 0.1.0 release review, so no live
+contract evidence was obtained. The adapter remains available for evaluation, but CryptoCompare is
+not part of the release-qualified provider set until its contract passes on a release commit.
## Specialized Modules
diff --git a/docs/getting-started/provider-selection.md b/docs/getting-started/provider-selection.md
index 35fc4e01..a62d3e90 100644
--- a/docs/getting-started/provider-selection.md
+++ b/docs/getting-started/provider-selection.md
@@ -2,6 +2,9 @@
Choose the right data provider for your needs with this decision flowchart.
+CryptoCompare is included for evaluation but is not release-qualified for 0.1.0. Account
+registration was unavailable during release validation, so no live contract evidence was obtained.
+
## Quick Decision Flowchart
```mermaid
@@ -17,7 +20,7 @@ flowchart TD
%% Crypto Path
CryptoChoice -->|No API key| CoinGecko[CoinGecko
Public API]
- CryptoChoice -->|API key is acceptable| CryptoCompare[CryptoCompare
Free/Paid]
+ CryptoChoice -->|API key is acceptable| CryptoCompare[CryptoCompare
Unverified for 0.1.0]
%% US Stocks Path
USChoice -->|FREE only| USFree{Call volume?}
@@ -64,7 +67,7 @@ flowchart TD
| Provider | Crypto | US Stocks | Global Stocks | Forex | Futures | API Key | Free Tier | Best For |
|----------|--------|-----------|---------------|-------|---------|---------|-----------|----------|
| **CoinGecko** | ✅ | ❌ | ❌ | ❌ | ❌ | No | Unlimited | Crypto historical |
-| **CryptoCompare** | ✅ | ❌ | ❌ | ❌ | ❌ | Yes | Good | Crypto real-time |
+| **CryptoCompare** | ✅ | ❌ | ❌ | ❌ | ❌ | Yes | Unverified | Evaluation only |
| **Tiingo** | ✅ | ✅ | ❌ | ❌ | ❌ | Yes | 1000/day | High-quality stocks |
| **EODHD** | ❌ | ✅ | ✅ | ❌ | ❌ | Yes | 20 calls/day, one-year history | Global stocks |
| **Finnhub** | ✅ | ✅ | ✅ | ✅ | ❌ | Yes | US quotes; OHLC is paid | Quotes and company data |
@@ -83,7 +86,6 @@ flowchart TD
| **Tiingo** | 1000 calls | 500 symbols | Daily stock updates |
| **EODHD** | 20 calls | Account limit | Global stock testing |
| **Twelve Data** | 800 calls | ~24K calls | Multi-asset research |
-| **CryptoCompare** | Varies | Varies | Crypto real-time |
| **Massive Stocks Basic** | 5 calls/minute | Account limit | US aggregate data |
#### Paid Tiers (Affordable)
diff --git a/docs/providers/cryptocompare.md b/docs/providers/cryptocompare.md
index 36e8b127..5234f2c7 100644
--- a/docs/providers/cryptocompare.md
+++ b/docs/providers/cryptocompare.md
@@ -3,13 +3,15 @@
**Provider**: `CryptoCompareProvider`
**Website**: [cryptocompare.com](https://www.cryptocompare.com)
**API Key**: Required
-**Free Tier**: 250,000 calls/month
+**0.1.0 status**: Included for evaluation; not release-qualified
---
## Overview
-CryptoCompare provides comprehensive cryptocurrency historical data with good coverage and reasonable free tier.
+CryptoCompare account registration was unavailable during the 0.1.0 release review. The adapter is
+included for evaluation, but it has no successful live contract evidence for this release. Do not
+treat it as a release-qualified provider until a later release records a successful contract run.
**Best For**: Crypto historical data, alternative to Binance
@@ -60,8 +62,7 @@ Get your API key at [cryptocompare.com/cryptopian/api-keys](https://www.cryptoco
## Rate Limits
-- Free: 250,000 calls/month
-- Paid: $19.99+/mo for higher limits
+Consult CryptoCompare's current terms before use. Access and limits were not verified for 0.1.0.
---
diff --git a/docs/providers/index.md b/docs/providers/index.md
index 96e409cc..3885a27b 100644
--- a/docs/providers/index.md
+++ b/docs/providers/index.md
@@ -25,7 +25,7 @@ ML4T Data supports 20+ live and specialized data providers, plus synthetic and t
| [Finnhub](finnhub.md) | US quotes; premium OHLCV | 60 requests/minute | Thread | Yes |
| [Binance](binance.md) | Crypto | Unlimited | Native | No |
| [OKX](okx.md) | Crypto Perpetuals | No geo-limits | Native | No |
-| [CryptoCompare](cryptocompare.md) | Crypto | Good | Native | Required |
+| [CryptoCompare](cryptocompare.md) | Crypto | Unverified for 0.1.0 | Native | Required |
| [Oanda](oanda.md) | Forex | Demo only | Thread | Yes |
## Async Support
@@ -83,6 +83,8 @@ provider's capabilities before passing it to `DataManager` or `async_batch_load(
Provider access, coverage, retention, and redistribution terms can differ by account tier. Confirm
the provider page and the provider's current terms before selecting it for a production dataset.
+CryptoCompare is included for evaluation but is not release-qualified for 0.1.0 because account
+registration was unavailable during release validation and no live contract evidence was obtained.
## Authentication
diff --git a/scripts/verify_provider_contract_runs.py b/scripts/verify_provider_contract_runs.py
index 2e972bdb..6d296b38 100644
--- a/scripts/verify_provider_contract_runs.py
+++ b/scripts/verify_provider_contract_runs.py
@@ -15,10 +15,12 @@
"tiingo",
"massive",
"finnhub",
- "cryptocompare",
"databento",
"oanda",
)
+UNVERIFIED_CONTRACTS = {
+ "cryptocompare": "account registration unavailable during 0.1.0 release validation",
+}
RUN_TITLE_PREFIX = "Provider Contract - "
@@ -89,6 +91,8 @@ def main() -> None:
)
for provider in REQUIRED_CONTRACTS:
print(f"{provider}: {successful[provider]}")
+ for provider, reason in UNVERIFIED_CONTRACTS.items():
+ print(f"{provider}: unverified - {reason}")
if __name__ == "__main__":
diff --git a/src/ml4t/data/providers/alpaca.py b/src/ml4t/data/providers/alpaca.py
index c939ecc4..e535438d 100644
--- a/src/ml4t/data/providers/alpaca.py
+++ b/src/ml4t/data/providers/alpaca.py
@@ -214,10 +214,12 @@ def __init__(
AuthenticationError: If either credential is missing.
DataValidationError: If ``feed`` is not a supported value.
"""
- self.api_key = api_key or os.getenv("ALPACA_API_KEY") or os.getenv("APCA_API_KEY_ID")
- self.api_secret = (
+ resolved_api_key = api_key or os.getenv("ALPACA_API_KEY") or os.getenv("APCA_API_KEY_ID")
+ resolved_api_secret = (
api_secret or os.getenv("ALPACA_API_SECRET") or os.getenv("APCA_API_SECRET_KEY")
)
+ self.api_key = resolved_api_key.strip() if resolved_api_key else None
+ self.api_secret = resolved_api_secret.strip() if resolved_api_secret else None
if not self.api_key or not self.api_secret:
raise AuthenticationError(
provider="alpaca",
diff --git a/tests/test_alpaca_provider.py b/tests/test_alpaca_provider.py
index 870fb633..e39dffd8 100644
--- a/tests/test_alpaca_provider.py
+++ b/tests/test_alpaca_provider.py
@@ -79,6 +79,23 @@ def test_init_with_env_keys(self):
assert provider.api_key == "k"
assert provider.api_secret == "s"
+ def test_credentials_strip_surrounding_whitespace(self):
+ """Copied credentials cannot introduce invalid HTTP header whitespace."""
+ provider = AlpacaDataProvider(api_key=" k\n", api_secret="s\r\n")
+
+ assert provider.api_key == "k"
+ assert provider.api_secret == "s"
+ assert provider._auth_headers == {
+ "APCA-API-KEY-ID": "k",
+ "APCA-API-SECRET-KEY": "s",
+ }
+
+ @pytest.mark.parametrize("credential", ["", " ", "\r\n"])
+ def test_whitespace_only_credentials_raise(self, credential):
+ """Credentials that normalize to empty are rejected locally."""
+ with patch.dict("os.environ", {}, clear=True), pytest.raises(AuthenticationError):
+ AlpacaDataProvider(api_key=credential, api_secret="s")
+
def test_init_with_apca_env_fallback(self):
"""Alpaca SDK/CLI env names are honored as a fallback."""
with patch.dict(
diff --git a/tests/test_provider_contract_verification.py b/tests/test_provider_contract_verification.py
index 89fdaec7..78cd88dd 100644
--- a/tests/test_provider_contract_verification.py
+++ b/tests/test_provider_contract_verification.py
@@ -4,7 +4,11 @@
import pytest
-from scripts.verify_provider_contract_runs import REQUIRED_CONTRACTS, validate_contract_runs
+from scripts.verify_provider_contract_runs import (
+ REQUIRED_CONTRACTS,
+ UNVERIFIED_CONTRACTS,
+ validate_contract_runs,
+)
def _run(provider: str, commit: str, conclusion: str = "success") -> dict[str, str]:
@@ -25,6 +29,11 @@ def test_all_required_contracts_must_pass_for_the_release_commit() -> None:
assert tuple(successful) == REQUIRED_CONTRACTS
+def test_unverified_cryptocompare_is_not_release_qualified() -> None:
+ assert set(UNVERIFIED_CONTRACTS) == {"cryptocompare"}
+ assert set(REQUIRED_CONTRACTS).isdisjoint(UNVERIFIED_CONTRACTS)
+
+
def test_contracts_from_another_commit_do_not_satisfy_release_policy() -> None:
commit = "a" * 40
runs = [_run(provider, "b" * 40) for provider in REQUIRED_CONTRACTS]