Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 5 additions & 3 deletions docs/getting-started/provider-selection.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -17,7 +20,7 @@ flowchart TD

%% Crypto Path
CryptoChoice -->|No API key| CoinGecko[CoinGecko<br/>Public API]
CryptoChoice -->|API key is acceptable| CryptoCompare[CryptoCompare<br/>Free/Paid]
CryptoChoice -->|API key is acceptable| CryptoCompare[CryptoCompare<br/>Unverified for 0.1.0]

%% US Stocks Path
USChoice -->|FREE only| USFree{Call volume?}
Expand Down Expand Up @@ -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 |
Comment on lines 67 to 72
| **Finnhub** | ✅ | ✅ | ✅ | ✅ | ❌ | Yes | US quotes; OHLC is paid | Quotes and company data |
Expand All @@ -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)
Expand Down
9 changes: 5 additions & 4 deletions docs/providers/cryptocompare.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.

---

Expand Down
4 changes: 3 additions & 1 deletion docs/providers/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Comment on lines 25 to 29

## Async Support
Expand Down Expand Up @@ -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

Expand Down
6 changes: 5 additions & 1 deletion scripts/verify_provider_contract_runs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 - "


Expand Down Expand Up @@ -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__":
Expand Down
6 changes: 4 additions & 2 deletions src/ml4t/data/providers/alpaca.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
17 changes: 17 additions & 0 deletions tests/test_alpaca_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
11 changes: 10 additions & 1 deletion tests/test_provider_contract_verification.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand All @@ -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]
Expand Down
Loading