Swap raw requests for Twelve Data Python SDK
data_provider now uses TDClient.time_series().as_pandas() instead of hand-rolled REST calls. Throttle, validation, and parquet caching are unchanged. Adds twelvedata>=1.2.0 to requirements.txt.
This commit is contained in:
+37
-46
@@ -1,10 +1,10 @@
|
|||||||
"""
|
"""
|
||||||
ForexBot2 — Data Provider (Twelve Data, USD/JPY only)
|
ForexBot2 — Data Provider (Twelve Data SDK, USD/JPY only)
|
||||||
=======================================================
|
==========================================================
|
||||||
Fetches and validates OHLCV from Twelve Data. No synthetic fallback —
|
Fetches and validates OHLCV via the official `twelvedata` Python SDK.
|
||||||
if the API fails the caller learns about it. Adds a parquet on-disk
|
No synthetic fallback — if the API fails the caller learns about it.
|
||||||
cache so the backtest worker pool reuses one download instead of
|
Adds a parquet on-disk cache so the backtest worker pool reuses one
|
||||||
hammering Twelve Data 27 times during a parameter sweep.
|
download instead of hammering Twelve Data during a parameter sweep.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -14,7 +14,7 @@ from collections import deque
|
|||||||
from typing import Dict, Optional
|
from typing import Dict, Optional
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import requests
|
from twelvedata import TDClient
|
||||||
|
|
||||||
from config import PlatformConfig, Timeframe, USDJPY, DISPLAY, PIP_VALUE
|
from config import PlatformConfig, Timeframe, USDJPY, DISPLAY, PIP_VALUE
|
||||||
|
|
||||||
@@ -27,9 +27,7 @@ class DataIntegrityError(Exception):
|
|||||||
|
|
||||||
|
|
||||||
class ForexDataProvider:
|
class ForexDataProvider:
|
||||||
"""Twelve Data REST client + on-disk parquet cache."""
|
"""Twelve Data SDK client + on-disk parquet cache."""
|
||||||
|
|
||||||
TD_BASE_URL = "https://api.twelvedata.com"
|
|
||||||
|
|
||||||
TD_INTERVALS = {
|
TD_INTERVALS = {
|
||||||
Timeframe.M1: "1min",
|
Timeframe.M1: "1min",
|
||||||
@@ -66,10 +64,9 @@ class ForexDataProvider:
|
|||||||
self._cache: Dict[str, pd.DataFrame] = {}
|
self._cache: Dict[str, pd.DataFrame] = {}
|
||||||
self._cache_timestamps: Dict[str, float] = {}
|
self._cache_timestamps: Dict[str, float] = {}
|
||||||
self._request_times: deque = deque()
|
self._request_times: deque = deque()
|
||||||
self._session = requests.Session()
|
self._td: Optional[TDClient] = (
|
||||||
self._session.headers.update({
|
TDClient(apikey=self._api_key) if self._api_key else None
|
||||||
"User-Agent": "Mozilla/5.0 (compatible; ForexIntel/1.0)"
|
)
|
||||||
})
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _to_td_symbol(symbol: str) -> str:
|
def _to_td_symbol(symbol: str) -> str:
|
||||||
@@ -230,7 +227,7 @@ class ForexDataProvider:
|
|||||||
start_date: Optional[str] = None,
|
start_date: Optional[str] = None,
|
||||||
end_date: Optional[str] = None,
|
end_date: Optional[str] = None,
|
||||||
) -> pd.DataFrame:
|
) -> pd.DataFrame:
|
||||||
if not self._api_key:
|
if self._td is None:
|
||||||
raise DataIntegrityError(
|
raise DataIntegrityError(
|
||||||
"No Twelve Data API key. Set TWELVE_DATA_API_KEY env var "
|
"No Twelve Data API key. Set TWELVE_DATA_API_KEY env var "
|
||||||
"or config.twelve_data_api_key."
|
"or config.twelve_data_api_key."
|
||||||
@@ -241,50 +238,44 @@ class ForexDataProvider:
|
|||||||
|
|
||||||
self._throttle(credits=1)
|
self._throttle(credits=1)
|
||||||
|
|
||||||
params = {
|
kwargs = {
|
||||||
"symbol": td_symbol,
|
"symbol": td_symbol,
|
||||||
"interval": interval,
|
"interval": interval,
|
||||||
"outputsize": outputsize,
|
"outputsize": min(outputsize, 5000),
|
||||||
"apikey": self._api_key,
|
|
||||||
"timezone": "UTC",
|
"timezone": "UTC",
|
||||||
"format": "json",
|
|
||||||
}
|
}
|
||||||
if start_date:
|
if start_date:
|
||||||
params["start_date"] = start_date
|
kwargs["start_date"] = start_date
|
||||||
if end_date:
|
if end_date:
|
||||||
params["end_date"] = end_date
|
kwargs["end_date"] = end_date
|
||||||
|
|
||||||
resp = self._session.get(
|
try:
|
||||||
f"{self.TD_BASE_URL}/time_series",
|
df = self._td.time_series(**kwargs).as_pandas()
|
||||||
params=params,
|
except Exception as e:
|
||||||
timeout=30,
|
|
||||||
)
|
|
||||||
resp.raise_for_status()
|
|
||||||
data = resp.json()
|
|
||||||
|
|
||||||
if data.get("status") == "error":
|
|
||||||
raise DataIntegrityError(
|
raise DataIntegrityError(
|
||||||
f"Twelve Data error for {td_symbol}: {data.get('message', 'unknown')}"
|
f"Twelve Data fetch failed for {td_symbol}: {e}"
|
||||||
)
|
) from e
|
||||||
|
|
||||||
values = data.get("values")
|
if df is None or df.empty:
|
||||||
if not values:
|
|
||||||
raise DataIntegrityError(f"Empty response from Twelve Data for {td_symbol}")
|
raise DataIntegrityError(f"Empty response from Twelve Data for {td_symbol}")
|
||||||
|
|
||||||
records = [
|
# SDK returns lowercase columns; engine expects capitalised.
|
||||||
{
|
df = df.rename(columns={
|
||||||
"Datetime": pd.Timestamp(v["datetime"], tz="UTC"),
|
"open": "Open", "high": "High", "low": "Low",
|
||||||
"Open": float(v["open"]),
|
"close": "Close", "volume": "Volume",
|
||||||
"High": float(v["high"]),
|
})
|
||||||
"Low": float(v["low"]),
|
for col in ("Open", "High", "Low", "Close", "Volume"):
|
||||||
"Close": float(v["close"]),
|
if col in df.columns:
|
||||||
"Volume": float(v.get("volume") or 0),
|
df[col] = pd.to_numeric(df[col], errors="coerce")
|
||||||
}
|
if "Volume" not in df.columns:
|
||||||
for v in reversed(values)
|
df["Volume"] = 0.0
|
||||||
]
|
|
||||||
|
|
||||||
df = pd.DataFrame(records).set_index("Datetime")
|
# SDK returns newest-first; engine + indicators want oldest-first.
|
||||||
|
df = df.sort_index()
|
||||||
|
if df.index.tz is None:
|
||||||
|
df.index = df.index.tz_localize("UTC")
|
||||||
df.index.name = "Datetime"
|
df.index.name = "Datetime"
|
||||||
|
|
||||||
logger.debug(f"Fetched {len(df)} bars for {td_symbol} @ {interval}")
|
logger.debug(f"Fetched {len(df)} bars for {td_symbol} @ {interval}")
|
||||||
return df
|
return df
|
||||||
|
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ pandas>=2.0
|
|||||||
requests>=2.28
|
requests>=2.28
|
||||||
pyarrow>=14.0
|
pyarrow>=14.0
|
||||||
tqdm>=4.66
|
tqdm>=4.66
|
||||||
|
twelvedata>=1.2.0
|
||||||
|
|||||||
Reference in New Issue
Block a user