arithmaxchest 0.1.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.
- arithmaxchest-0.1.0/LICENSE +21 -0
- arithmaxchest-0.1.0/PKG-INFO +60 -0
- arithmaxchest-0.1.0/README.md +34 -0
- arithmaxchest-0.1.0/achest/__init__.py +5 -0
- arithmaxchest-0.1.0/achest/client.py +41 -0
- arithmaxchest-0.1.0/achest/server.py +79 -0
- arithmaxchest-0.1.0/achest/service.py +161 -0
- arithmaxchest-0.1.0/arithmaxchest.egg-info/PKG-INFO +60 -0
- arithmaxchest-0.1.0/arithmaxchest.egg-info/SOURCES.txt +13 -0
- arithmaxchest-0.1.0/arithmaxchest.egg-info/dependency_links.txt +1 -0
- arithmaxchest-0.1.0/arithmaxchest.egg-info/requires.txt +22 -0
- arithmaxchest-0.1.0/arithmaxchest.egg-info/top_level.txt +1 -0
- arithmaxchest-0.1.0/pyproject.toml +27 -0
- arithmaxchest-0.1.0/setup.cfg +4 -0
- arithmaxchest-0.1.0/tests/test_routing.py +18 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Arithmax Research
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: arithmaxchest
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Centralized market data API and Python client
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: fastapi>=0.110
|
|
9
|
+
Requires-Dist: httpx>=0.27
|
|
10
|
+
Requires-Dist: pandas>=2.0
|
|
11
|
+
Requires-Dist: pyarrow>=14.0
|
|
12
|
+
Requires-Dist: requests>=2.31
|
|
13
|
+
Provides-Extra: yahoo
|
|
14
|
+
Requires-Dist: yfinance>=0.2.40; extra == "yahoo"
|
|
15
|
+
Provides-Extra: futures
|
|
16
|
+
Requires-Dist: databento>=1.0; extra == "futures"
|
|
17
|
+
Provides-Extra: server
|
|
18
|
+
Requires-Dist: uvicorn[standard]>=0.29; extra == "server"
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
21
|
+
Provides-Extra: all
|
|
22
|
+
Requires-Dist: yfinance>=0.2.40; extra == "all"
|
|
23
|
+
Requires-Dist: databento>=1.0; extra == "all"
|
|
24
|
+
Requires-Dist: uvicorn[standard]>=0.29; extra == "all"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# Arithmax Chest
|
|
28
|
+
|
|
29
|
+
A standalone FastAPI service and Python client for querying centralized market data. Provider API keys stay on the server; research projects use only the client URL and token.
|
|
30
|
+
|
|
31
|
+
## Server
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
python -m pip install -e '.[all]'
|
|
35
|
+
export DATA_API_TOKEN='private-client-token'
|
|
36
|
+
export BINANCE_API_KEY='server-only-if-required'
|
|
37
|
+
export POLYGON_API_KEY='server-only'
|
|
38
|
+
export DATABENTO_API_KEY='server-only'
|
|
39
|
+
uvicorn achest.server:app --host 0.0.0.0 --port 8000
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Client
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from achest import MarketDataClient
|
|
46
|
+
|
|
47
|
+
with MarketDataClient('https://data.example.com', token='private-client-token') as client:
|
|
48
|
+
frame = client.get(['BTCUSDT', 'ES.FUT'], '2024-01-01', '2024-01-31')
|
|
49
|
+
client.download(['BTCUSDT'], '2024-01-01', '2024-01-31', 'data/btc.parquet')
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`auto` routes crypto to Binance, futures to Databento first, and equities to Yahoo. Supported response formats are JSON, CSV, and Parquet. Large downloads should be moved to a background job endpoint before production use.
|
|
53
|
+
|
|
54
|
+
## PyPI publishing
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
python -m pip install build twine
|
|
58
|
+
python -m build
|
|
59
|
+
python -m twine upload dist/*
|
|
60
|
+
```
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Arithmax Chest
|
|
2
|
+
|
|
3
|
+
A standalone FastAPI service and Python client for querying centralized market data. Provider API keys stay on the server; research projects use only the client URL and token.
|
|
4
|
+
|
|
5
|
+
## Server
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
python -m pip install -e '.[all]'
|
|
9
|
+
export DATA_API_TOKEN='private-client-token'
|
|
10
|
+
export BINANCE_API_KEY='server-only-if-required'
|
|
11
|
+
export POLYGON_API_KEY='server-only'
|
|
12
|
+
export DATABENTO_API_KEY='server-only'
|
|
13
|
+
uvicorn achest.server:app --host 0.0.0.0 --port 8000
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Client
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from achest import MarketDataClient
|
|
20
|
+
|
|
21
|
+
with MarketDataClient('https://data.example.com', token='private-client-token') as client:
|
|
22
|
+
frame = client.get(['BTCUSDT', 'ES.FUT'], '2024-01-01', '2024-01-31')
|
|
23
|
+
client.download(['BTCUSDT'], '2024-01-01', '2024-01-31', 'data/btc.parquet')
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`auto` routes crypto to Binance, futures to Databento first, and equities to Yahoo. Supported response formats are JSON, CSV, and Parquet. Large downloads should be moved to a background job endpoint before production use.
|
|
27
|
+
|
|
28
|
+
## PyPI publishing
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
python -m pip install build twine
|
|
32
|
+
python -m build
|
|
33
|
+
python -m twine upload dist/*
|
|
34
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Python client for the centralized market-data API."""
|
|
2
|
+
|
|
3
|
+
from datetime import date
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Iterable
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MarketDataClient:
|
|
12
|
+
def __init__(self, base_url: str, token: str | None = None, timeout: float = 300.0):
|
|
13
|
+
headers = {"Authorization": f"Bearer {token}"} if token else {}
|
|
14
|
+
self.client = httpx.Client(base_url=base_url.rstrip("/"), headers=headers, timeout=timeout)
|
|
15
|
+
|
|
16
|
+
def route(self, symbol: str, resolution: str = "daily", provider: str = "auto") -> dict:
|
|
17
|
+
response = self.client.get("/v1/route", params={"symbol": symbol, "resolution": resolution, "provider": provider})
|
|
18
|
+
response.raise_for_status()
|
|
19
|
+
return response.json()
|
|
20
|
+
|
|
21
|
+
def get(self, symbols: Iterable[str], start: date | str, end: date | str, resolution: str = "daily", provider: str = "auto") -> pd.DataFrame:
|
|
22
|
+
response = self.client.post("/v1/data", json={"symbols": list(symbols), "start": str(start), "end": str(end), "resolution": resolution, "provider": provider, "format": "json"})
|
|
23
|
+
response.raise_for_status()
|
|
24
|
+
return pd.DataFrame(response.json())
|
|
25
|
+
|
|
26
|
+
def download(self, symbols: Iterable[str], start: date | str, end: date | str, output: str | Path, resolution: str = "daily", provider: str = "auto", format: str = "parquet") -> Path:
|
|
27
|
+
response = self.client.post("/v1/data", json={"symbols": list(symbols), "start": str(start), "end": str(end), "resolution": resolution, "provider": provider, "format": format})
|
|
28
|
+
response.raise_for_status()
|
|
29
|
+
destination = Path(output)
|
|
30
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
destination.write_bytes(response.content)
|
|
32
|
+
return destination
|
|
33
|
+
|
|
34
|
+
def close(self) -> None:
|
|
35
|
+
self.client.close()
|
|
36
|
+
|
|
37
|
+
def __enter__(self):
|
|
38
|
+
return self
|
|
39
|
+
|
|
40
|
+
def __exit__(self, exc_type, exc_value, traceback) -> None:
|
|
41
|
+
self.close()
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""FastAPI application for the centralized market-data service."""
|
|
2
|
+
|
|
3
|
+
from datetime import date
|
|
4
|
+
from io import BytesIO
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from fastapi import Depends, FastAPI, Header, HTTPException, Query
|
|
8
|
+
from fastapi.responses import Response
|
|
9
|
+
from pydantic import BaseModel, Field, field_validator
|
|
10
|
+
|
|
11
|
+
from .service import PROVIDER_CAPABILITIES, DataRequest, UnsupportedRequest, fetch, select_provider
|
|
12
|
+
|
|
13
|
+
app = FastAPI(title="Central Market Data API", version="0.1.0")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DownloadRequest(BaseModel):
|
|
17
|
+
symbols: list[str] = Field(min_length=1)
|
|
18
|
+
start: date
|
|
19
|
+
end: date
|
|
20
|
+
resolution: str = "daily"
|
|
21
|
+
provider: str = "auto"
|
|
22
|
+
format: str = "csv"
|
|
23
|
+
|
|
24
|
+
@field_validator("resolution")
|
|
25
|
+
@classmethod
|
|
26
|
+
def valid_resolution(cls, value: str) -> str:
|
|
27
|
+
if value not in {"tick", "second", "minute", "hour", "daily"}:
|
|
28
|
+
raise ValueError("unsupported resolution")
|
|
29
|
+
return value
|
|
30
|
+
|
|
31
|
+
@field_validator("format")
|
|
32
|
+
@classmethod
|
|
33
|
+
def valid_format(cls, value: str) -> str:
|
|
34
|
+
if value not in {"json", "csv", "parquet"}:
|
|
35
|
+
raise ValueError("format must be json, csv, or parquet")
|
|
36
|
+
return value
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def require_client_token(authorization: str | None = Header(default=None)) -> None:
|
|
40
|
+
expected = os.getenv("DATA_API_TOKEN")
|
|
41
|
+
if expected and authorization != f"Bearer {expected}":
|
|
42
|
+
raise HTTPException(status_code=401, detail="invalid client token")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@app.get("/health")
|
|
46
|
+
def health() -> dict[str, str]:
|
|
47
|
+
return {"status": "ok"}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@app.get("/v1/providers", dependencies=[Depends(require_client_token)])
|
|
51
|
+
def providers() -> dict:
|
|
52
|
+
return {"providers": PROVIDER_CAPABILITIES}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@app.get("/v1/route", dependencies=[Depends(require_client_token)])
|
|
56
|
+
def route(symbol: str, resolution: str = Query(default="daily"), provider: str = Query(default="auto")) -> dict[str, str]:
|
|
57
|
+
try:
|
|
58
|
+
selected = select_provider(symbol, provider, resolution)
|
|
59
|
+
except UnsupportedRequest as error:
|
|
60
|
+
raise HTTPException(status_code=422, detail=str(error)) from error
|
|
61
|
+
return {"symbol": symbol, "resolution": resolution, "provider": selected}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@app.post("/v1/data", dependencies=[Depends(require_client_token)])
|
|
65
|
+
def data(request: DownloadRequest) -> Response:
|
|
66
|
+
try:
|
|
67
|
+
frame = fetch(DataRequest(request.symbols, request.start, request.end, request.resolution, request.provider))
|
|
68
|
+
except (UnsupportedRequest, ValueError) as error:
|
|
69
|
+
raise HTTPException(status_code=422, detail=str(error)) from error
|
|
70
|
+
except Exception as error:
|
|
71
|
+
raise HTTPException(status_code=502, detail=f"provider request failed: {error}") from error
|
|
72
|
+
table = frame.reset_index(names="timestamp")
|
|
73
|
+
if request.format == "json":
|
|
74
|
+
return Response(table.to_json(orient="records", date_format="iso"), media_type="application/json")
|
|
75
|
+
if request.format == "csv":
|
|
76
|
+
return Response(table.to_csv(index=False), media_type="text/csv")
|
|
77
|
+
output = BytesIO()
|
|
78
|
+
table.to_parquet(output, index=False)
|
|
79
|
+
return Response(output.getvalue(), media_type="application/octet-stream", headers={"Content-Disposition": "attachment; filename=market-data.parquet"})
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Provider routing and normalized market-data retrieval."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import date, datetime, time
|
|
5
|
+
import os
|
|
6
|
+
from typing import Iterable
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class DataRequest:
|
|
14
|
+
symbols: list[str]
|
|
15
|
+
start: date
|
|
16
|
+
end: date
|
|
17
|
+
resolution: str = "daily"
|
|
18
|
+
provider: str = "auto"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
PROVIDER_CAPABILITIES = {
|
|
22
|
+
"yahoo": {"assets": {"equity", "etf", "index", "crypto"}, "resolutions": {"minute", "hour", "daily"}},
|
|
23
|
+
"binance": {"assets": {"crypto"}, "resolutions": {"minute", "hour", "daily"}},
|
|
24
|
+
"polygon": {"assets": {"futures"}, "resolutions": {"minute", "hour", "daily"}},
|
|
25
|
+
"databento": {"assets": {"futures"}, "resolutions": {"tick", "second", "minute", "hour", "daily"}},
|
|
26
|
+
}
|
|
27
|
+
FUTURES_ROOTS = {"ES", "NQ", "YM", "RTY", "CL", "GC", "SI", "ZB", "ZN", "NG", "ZS", "ZC", "ZW"}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class UnsupportedRequest(ValueError):
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def classify_symbol(symbol: str) -> str:
|
|
35
|
+
clean = symbol.upper().strip()
|
|
36
|
+
if clean.endswith(("USDT", "USDC", "-USD")):
|
|
37
|
+
return "crypto"
|
|
38
|
+
if clean.endswith(".FUT") or ".c." in clean or clean in FUTURES_ROOTS:
|
|
39
|
+
return "futures"
|
|
40
|
+
if clean.startswith("^"):
|
|
41
|
+
return "index"
|
|
42
|
+
return "equity"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def select_provider(symbol: str, requested: str, resolution: str) -> str:
|
|
46
|
+
asset = classify_symbol(symbol)
|
|
47
|
+
if requested != "auto":
|
|
48
|
+
capabilities = PROVIDER_CAPABILITIES.get(requested)
|
|
49
|
+
if not capabilities or asset not in capabilities["assets"] or resolution not in capabilities["resolutions"]:
|
|
50
|
+
raise UnsupportedRequest(f"Provider {requested!r} does not support {asset} at {resolution} resolution")
|
|
51
|
+
return requested
|
|
52
|
+
preferences = {
|
|
53
|
+
"futures": ["databento", "polygon"],
|
|
54
|
+
"crypto": ["binance", "yahoo"],
|
|
55
|
+
"equity": ["yahoo"],
|
|
56
|
+
"etf": ["yahoo"],
|
|
57
|
+
"index": ["yahoo"],
|
|
58
|
+
}[asset]
|
|
59
|
+
for provider in preferences:
|
|
60
|
+
if resolution in PROVIDER_CAPABILITIES[provider]["resolutions"]:
|
|
61
|
+
return provider
|
|
62
|
+
raise UnsupportedRequest(f"No provider supports {asset} at {resolution} resolution")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _as_datetime(value: date, end_of_day: bool = False) -> datetime:
|
|
66
|
+
return datetime.combine(value, time.max if end_of_day else time.min)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _frame_from_bars(bars: Iterable[dict]) -> pd.DataFrame:
|
|
70
|
+
frame = pd.DataFrame(list(bars))
|
|
71
|
+
if frame.empty:
|
|
72
|
+
return frame
|
|
73
|
+
frame["timestamp"] = pd.to_datetime(frame["timestamp"], utc=True)
|
|
74
|
+
return frame.set_index("timestamp").sort_index()[["open", "high", "low", "close", "volume"]]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _yahoo(symbol: str, request: DataRequest) -> pd.DataFrame:
|
|
78
|
+
import yfinance as yf
|
|
79
|
+
|
|
80
|
+
interval = {"minute": "1m", "hour": "1h", "daily": "1d"}[request.resolution]
|
|
81
|
+
history = yf.Ticker(symbol).history(
|
|
82
|
+
start=request.start.isoformat(), end=request.end.isoformat(), interval=interval, auto_adjust=False
|
|
83
|
+
)
|
|
84
|
+
if history.empty:
|
|
85
|
+
return pd.DataFrame()
|
|
86
|
+
history.index.name = "timestamp"
|
|
87
|
+
return history.rename(columns={"Open": "open", "High": "high", "Low": "low", "Close": "close", "Volume": "volume"})[
|
|
88
|
+
["open", "high", "low", "close", "volume"]
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _binance(symbol: str, request: DataRequest) -> pd.DataFrame:
|
|
93
|
+
interval = {"minute": "1m", "hour": "1h", "daily": "1d"}[request.resolution]
|
|
94
|
+
response = requests.get("https://api.binance.com/api/v3/klines", params={
|
|
95
|
+
"symbol": symbol.upper(), "interval": interval,
|
|
96
|
+
"startTime": int(_as_datetime(request.start).timestamp() * 1000),
|
|
97
|
+
"endTime": int(_as_datetime(request.end, True).timestamp() * 1000), "limit": 1000,
|
|
98
|
+
}, timeout=60)
|
|
99
|
+
response.raise_for_status()
|
|
100
|
+
return _frame_from_bars({
|
|
101
|
+
"timestamp": datetime.fromtimestamp(row[0] / 1000), "open": row[1], "high": row[2],
|
|
102
|
+
"low": row[3], "close": row[4], "volume": row[5],
|
|
103
|
+
} for row in response.json())
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _polygon(symbol: str, request: DataRequest) -> pd.DataFrame:
|
|
107
|
+
timespan = {"minute": "minute", "hour": "hour", "daily": "day"}[request.resolution]
|
|
108
|
+
key = os.getenv("POLYGON_API_KEY")
|
|
109
|
+
if not key:
|
|
110
|
+
raise RuntimeError("POLYGON_API_KEY is not configured on the server")
|
|
111
|
+
response = requests.get(
|
|
112
|
+
f"https://api.polygon.io/v2/aggs/ticker/{symbol}/range/1/{timespan}/{request.start}/{request.end}",
|
|
113
|
+
params={"apiKey": key, "limit": 50000}, timeout=120,
|
|
114
|
+
)
|
|
115
|
+
response.raise_for_status()
|
|
116
|
+
return _frame_from_bars({
|
|
117
|
+
"timestamp": datetime.fromtimestamp(row["t"] / 1000), "open": row["o"], "high": row["h"],
|
|
118
|
+
"low": row["l"], "close": row["c"], "volume": row.get("v", 0),
|
|
119
|
+
} for row in response.json().get("results", []))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _databento(symbol: str, request: DataRequest) -> pd.DataFrame:
|
|
123
|
+
import databento as db
|
|
124
|
+
from databento import Schema, SType
|
|
125
|
+
|
|
126
|
+
key = os.getenv("DATABENTO_API_KEY")
|
|
127
|
+
if not key:
|
|
128
|
+
raise RuntimeError("DATABENTO_API_KEY is not configured on the server")
|
|
129
|
+
continuous = symbol.replace(".FUT", ".c.0")
|
|
130
|
+
schema = {"tick": Schema.MBP_1, "second": Schema.OHLCV_1S, "minute": Schema.OHLCV_1M, "hour": Schema.OHLCV_1H, "daily": Schema.OHLCV_1D}[request.resolution]
|
|
131
|
+
data = db.Historical(key=key).timeseries.get_range(
|
|
132
|
+
dataset="GLBX.MDP3", symbols=continuous, schema=schema,
|
|
133
|
+
start=request.start.isoformat(), end=request.end.isoformat(),
|
|
134
|
+
stype_in=SType.CONTINUOUS if ".c." in continuous else SType.RAW_SYMBOL,
|
|
135
|
+
).to_df()
|
|
136
|
+
if data.empty:
|
|
137
|
+
return data
|
|
138
|
+
data = data.rename(columns={"ts_event": "timestamp"}).set_index("timestamp")
|
|
139
|
+
return data[["open", "high", "low", "close", "volume"]]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def fetch_symbol(request: DataRequest, symbol: str) -> tuple[str, pd.DataFrame]:
|
|
143
|
+
provider = select_provider(symbol, request.provider, request.resolution)
|
|
144
|
+
fetchers = {"yahoo": _yahoo, "binance": _binance, "polygon": _polygon, "databento": _databento}
|
|
145
|
+
frame = fetchers[provider](symbol, request)
|
|
146
|
+
if not frame.empty:
|
|
147
|
+
frame.index = pd.to_datetime(frame.index, utc=True)
|
|
148
|
+
frame.insert(0, "symbol", symbol)
|
|
149
|
+
frame.insert(1, "provider", provider)
|
|
150
|
+
return provider, frame
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def fetch(request: DataRequest) -> pd.DataFrame:
|
|
154
|
+
if request.start > request.end:
|
|
155
|
+
raise UnsupportedRequest("start must be before or equal to end")
|
|
156
|
+
if not request.symbols:
|
|
157
|
+
raise UnsupportedRequest("at least one symbol is required")
|
|
158
|
+
frames = [frame for symbol in request.symbols if not (frame := fetch_symbol(request, symbol)[1]).empty]
|
|
159
|
+
if not frames:
|
|
160
|
+
return pd.DataFrame(columns=["symbol", "provider", "open", "high", "low", "close", "volume"])
|
|
161
|
+
return pd.concat(frames).sort_index()
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: arithmaxchest
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Centralized market data API and Python client
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: fastapi>=0.110
|
|
9
|
+
Requires-Dist: httpx>=0.27
|
|
10
|
+
Requires-Dist: pandas>=2.0
|
|
11
|
+
Requires-Dist: pyarrow>=14.0
|
|
12
|
+
Requires-Dist: requests>=2.31
|
|
13
|
+
Provides-Extra: yahoo
|
|
14
|
+
Requires-Dist: yfinance>=0.2.40; extra == "yahoo"
|
|
15
|
+
Provides-Extra: futures
|
|
16
|
+
Requires-Dist: databento>=1.0; extra == "futures"
|
|
17
|
+
Provides-Extra: server
|
|
18
|
+
Requires-Dist: uvicorn[standard]>=0.29; extra == "server"
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
21
|
+
Provides-Extra: all
|
|
22
|
+
Requires-Dist: yfinance>=0.2.40; extra == "all"
|
|
23
|
+
Requires-Dist: databento>=1.0; extra == "all"
|
|
24
|
+
Requires-Dist: uvicorn[standard]>=0.29; extra == "all"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# Arithmax Chest
|
|
28
|
+
|
|
29
|
+
A standalone FastAPI service and Python client for querying centralized market data. Provider API keys stay on the server; research projects use only the client URL and token.
|
|
30
|
+
|
|
31
|
+
## Server
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
python -m pip install -e '.[all]'
|
|
35
|
+
export DATA_API_TOKEN='private-client-token'
|
|
36
|
+
export BINANCE_API_KEY='server-only-if-required'
|
|
37
|
+
export POLYGON_API_KEY='server-only'
|
|
38
|
+
export DATABENTO_API_KEY='server-only'
|
|
39
|
+
uvicorn achest.server:app --host 0.0.0.0 --port 8000
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Client
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from achest import MarketDataClient
|
|
46
|
+
|
|
47
|
+
with MarketDataClient('https://data.example.com', token='private-client-token') as client:
|
|
48
|
+
frame = client.get(['BTCUSDT', 'ES.FUT'], '2024-01-01', '2024-01-31')
|
|
49
|
+
client.download(['BTCUSDT'], '2024-01-01', '2024-01-31', 'data/btc.parquet')
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`auto` routes crypto to Binance, futures to Databento first, and equities to Yahoo. Supported response formats are JSON, CSV, and Parquet. Large downloads should be moved to a background job endpoint before production use.
|
|
53
|
+
|
|
54
|
+
## PyPI publishing
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
python -m pip install build twine
|
|
58
|
+
python -m build
|
|
59
|
+
python -m twine upload dist/*
|
|
60
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
achest/__init__.py
|
|
5
|
+
achest/client.py
|
|
6
|
+
achest/server.py
|
|
7
|
+
achest/service.py
|
|
8
|
+
arithmaxchest.egg-info/PKG-INFO
|
|
9
|
+
arithmaxchest.egg-info/SOURCES.txt
|
|
10
|
+
arithmaxchest.egg-info/dependency_links.txt
|
|
11
|
+
arithmaxchest.egg-info/requires.txt
|
|
12
|
+
arithmaxchest.egg-info/top_level.txt
|
|
13
|
+
tests/test_routing.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
fastapi>=0.110
|
|
2
|
+
httpx>=0.27
|
|
3
|
+
pandas>=2.0
|
|
4
|
+
pyarrow>=14.0
|
|
5
|
+
requests>=2.31
|
|
6
|
+
|
|
7
|
+
[all]
|
|
8
|
+
yfinance>=0.2.40
|
|
9
|
+
databento>=1.0
|
|
10
|
+
uvicorn[standard]>=0.29
|
|
11
|
+
|
|
12
|
+
[dev]
|
|
13
|
+
pytest>=8.0
|
|
14
|
+
|
|
15
|
+
[futures]
|
|
16
|
+
databento>=1.0
|
|
17
|
+
|
|
18
|
+
[server]
|
|
19
|
+
uvicorn[standard]>=0.29
|
|
20
|
+
|
|
21
|
+
[yahoo]
|
|
22
|
+
yfinance>=0.2.40
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
achest
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "arithmaxchest"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Centralized market data API and Python client"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"fastapi>=0.110",
|
|
13
|
+
"httpx>=0.27",
|
|
14
|
+
"pandas>=2.0",
|
|
15
|
+
"pyarrow>=14.0",
|
|
16
|
+
"requests>=2.31",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.optional-dependencies]
|
|
20
|
+
yahoo = ["yfinance>=0.2.40"]
|
|
21
|
+
futures = ["databento>=1.0"]
|
|
22
|
+
server = ["uvicorn[standard]>=0.29"]
|
|
23
|
+
dev = ["pytest>=8.0"]
|
|
24
|
+
all = ["yfinance>=0.2.40", "databento>=1.0", "uvicorn[standard]>=0.29"]
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.packages.find]
|
|
27
|
+
include = ["achest*"]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from achest.service import select_provider
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_crypto_routes_to_binance():
|
|
5
|
+
assert select_provider("BTCUSDT", "auto", "daily") == "binance"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_futures_routes_to_databento():
|
|
9
|
+
assert select_provider("ES.FUT", "auto", "minute") == "databento"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_yahoo_cannot_be_used_for_futures():
|
|
13
|
+
try:
|
|
14
|
+
select_provider("ES.FUT", "yahoo", "daily")
|
|
15
|
+
except ValueError as error:
|
|
16
|
+
assert "does not support futures" in str(error)
|
|
17
|
+
else:
|
|
18
|
+
raise AssertionError("Yahoo must not be accepted for futures")
|