191 lines
6.5 KiB
Python
191 lines
6.5 KiB
Python
|
|
"""
|
|||
|
|
外汇数据源
|
|||
|
|
使用 Tiingo 获取外汇数据
|
|||
|
|
"""
|
|||
|
|
from typing import Dict, List, Any, Optional
|
|||
|
|
from datetime import datetime, timedelta
|
|||
|
|
import time
|
|||
|
|
import requests
|
|||
|
|
|
|||
|
|
from app.data_sources.base import BaseDataSource, TIMEFRAME_SECONDS
|
|||
|
|
from app.utils.logger import get_logger
|
|||
|
|
from app.config import TiingoConfig, APIKeys
|
|||
|
|
|
|||
|
|
logger = get_logger(__name__)
|
|||
|
|
|
|||
|
|
|
|||
|
|
class ForexDataSource(BaseDataSource):
|
|||
|
|
"""外汇数据源 (Tiingo)"""
|
|||
|
|
|
|||
|
|
name = "Forex/Tiingo"
|
|||
|
|
|
|||
|
|
# Tiingo resampleFreq 映射
|
|||
|
|
# Tiingo 支持: 1min, 5min, 15min, 30min, 1hour, 4hour, 1day 等
|
|||
|
|
TIMEFRAME_MAP = {
|
|||
|
|
'1m': '1min',
|
|||
|
|
'5m': '5min',
|
|||
|
|
'15m': '15min',
|
|||
|
|
'30m': '30min',
|
|||
|
|
'1H': '1hour',
|
|||
|
|
'4H': '4hour',
|
|||
|
|
'1D': '1day',
|
|||
|
|
'1W': '1week',
|
|||
|
|
'1M': '1month'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 外汇对映射 (Tiingo 使用标准 ticker,如 eurusd, audusd)
|
|||
|
|
# 大写也可以,Tiingo 通常不区分大小写,但建议统一
|
|||
|
|
SYMBOL_MAP = {
|
|||
|
|
# 贵金属 (Tiingo 不一定支持所有 OANDA 格式的贵金属,通常是 XAUUSD)
|
|||
|
|
'XAUUSD': 'xauusd',
|
|||
|
|
'XAGUSD': 'xagusd',
|
|||
|
|
# 主要货币对
|
|||
|
|
'EURUSD': 'eurusd',
|
|||
|
|
'GBPUSD': 'gbpusd',
|
|||
|
|
'USDJPY': 'usdjpy',
|
|||
|
|
'AUDUSD': 'audusd',
|
|||
|
|
'USDCAD': 'usdcad',
|
|||
|
|
'USDCHF': 'usdchf',
|
|||
|
|
'NZDUSD': 'nzdusd',
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def __init__(self):
|
|||
|
|
self.base_url = TiingoConfig.BASE_URL
|
|||
|
|
if not APIKeys.TIINGO_API_KEY:
|
|||
|
|
logger.warning("Tiingo API key is not configured; FX data will be unavailable")
|
|||
|
|
|
|||
|
|
def _get_timeframe_seconds(self, timeframe: str) -> int:
|
|||
|
|
"""获取时间周期对应的秒数"""
|
|||
|
|
return TIMEFRAME_SECONDS.get(timeframe, 86400)
|
|||
|
|
|
|||
|
|
def get_kline(
|
|||
|
|
self,
|
|||
|
|
symbol: str,
|
|||
|
|
timeframe: str,
|
|||
|
|
limit: int,
|
|||
|
|
before_time: Optional[int] = None
|
|||
|
|
) -> List[Dict[str, Any]]:
|
|||
|
|
"""
|
|||
|
|
获取外汇K线数据
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
symbol: 外汇对代码(如 XAUUSD, EURUSD)
|
|||
|
|
timeframe: 时间周期
|
|||
|
|
limit: 数据条数
|
|||
|
|
before_time: 结束时间戳
|
|||
|
|
"""
|
|||
|
|
# 动态获取 API Key
|
|||
|
|
api_key = APIKeys.TIINGO_API_KEY
|
|||
|
|
if not api_key:
|
|||
|
|
logger.error("Tiingo API key is not configured")
|
|||
|
|
return []
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
# 1. 解析 Symbol
|
|||
|
|
tiingo_symbol = self.SYMBOL_MAP.get(symbol)
|
|||
|
|
if not tiingo_symbol:
|
|||
|
|
# 尝试智能转换: EURUSD -> eurusd
|
|||
|
|
tiingo_symbol = symbol.lower()
|
|||
|
|
|
|||
|
|
# 2. 解析 Resolution (resampleFreq)
|
|||
|
|
resample_freq = self.TIMEFRAME_MAP.get(timeframe)
|
|||
|
|
if not resample_freq:
|
|||
|
|
logger.warning(f"Tiingo does not support timeframe: {timeframe}")
|
|||
|
|
return []
|
|||
|
|
|
|||
|
|
# 3. 计算时间范围
|
|||
|
|
if before_time:
|
|||
|
|
end_dt = datetime.fromtimestamp(before_time)
|
|||
|
|
else:
|
|||
|
|
end_dt = datetime.now()
|
|||
|
|
|
|||
|
|
# 根据周期和数量计算开始时间
|
|||
|
|
tf_seconds = self._get_timeframe_seconds(timeframe)
|
|||
|
|
# 多取一些缓冲时间
|
|||
|
|
start_dt = end_dt - timedelta(seconds=limit * tf_seconds * 2)
|
|||
|
|
|
|||
|
|
# 格式化日期为 YYYY-MM-DD (Tiingo 支持该格式)
|
|||
|
|
start_date_str = start_dt.strftime('%Y-%m-%d')
|
|||
|
|
end_date_str = end_dt.strftime('%Y-%m-%d')
|
|||
|
|
|
|||
|
|
# 4. API 请求
|
|||
|
|
# URL: https://api.tiingo.com/tiingo/fx/{ticker}/prices
|
|||
|
|
url = f"{self.base_url}/fx/{tiingo_symbol}/prices"
|
|||
|
|
|
|||
|
|
params = {
|
|||
|
|
'startDate': start_date_str,
|
|||
|
|
'endDate': end_date_str,
|
|||
|
|
'resampleFreq': resample_freq,
|
|||
|
|
'token': api_key,
|
|||
|
|
'format': 'json'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# logger.info(f"Tiingo Request: {url} params={params}")
|
|||
|
|
|
|||
|
|
response = requests.get(url, params=params, timeout=TiingoConfig.TIMEOUT)
|
|||
|
|
|
|||
|
|
if response.status_code == 403: # 具体的权限错误
|
|||
|
|
logger.error("Tiingo API permission error (403): check whether your API key is valid and has access to this dataset.")
|
|||
|
|
return []
|
|||
|
|
|
|||
|
|
response.raise_for_status()
|
|||
|
|
data = response.json()
|
|||
|
|
|
|||
|
|
# 5. 处理响应
|
|||
|
|
# Tiingo returns a list of dicts:
|
|||
|
|
# [
|
|||
|
|
# {
|
|||
|
|
# "date": "2023-01-01T00:00:00.000Z",
|
|||
|
|
# "ticker": "eurusd",
|
|||
|
|
# "open": 1.07,
|
|||
|
|
# "high": 1.08,
|
|||
|
|
# "low": 1.06,
|
|||
|
|
# "close": 1.07
|
|||
|
|
# "mid": ... (optional, depends on settings, usually OHLC are bid or mid)
|
|||
|
|
# }, ...
|
|||
|
|
# ]
|
|||
|
|
# Note: Tiingo FX prices objects keys: date, open, high, low, close.
|
|||
|
|
|
|||
|
|
if not isinstance(data, list):
|
|||
|
|
logger.warning(f"Tiingo response is not a list: {data}")
|
|||
|
|
return []
|
|||
|
|
|
|||
|
|
klines = []
|
|||
|
|
for item in data:
|
|||
|
|
# 解析时间: "2023-01-01T00:00:00.000Z"
|
|||
|
|
dt_str = item.get('date')
|
|||
|
|
# 简化处理,Tiingo 返回的是 UTC 时间 ISO 格式
|
|||
|
|
# datetime.fromisoformat 在 Py3.7+ 支持,但要注意 Z 的处理
|
|||
|
|
# 这里简单处理一下 Z
|
|||
|
|
if dt_str.endswith('Z'):
|
|||
|
|
dt_str = dt_str[:-1]
|
|||
|
|
|
|||
|
|
dt = datetime.fromisoformat(dt_str)
|
|||
|
|
ts = int(dt.timestamp())
|
|||
|
|
|
|||
|
|
klines.append({
|
|||
|
|
'time': ts,
|
|||
|
|
'open': float(item.get('open')),
|
|||
|
|
'high': float(item.get('high')),
|
|||
|
|
'low': float(item.get('low')),
|
|||
|
|
'close': float(item.get('close')),
|
|||
|
|
'volume': 0.0 # Tiingo FX 通常没有 volume
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
# 按时间排序
|
|||
|
|
klines.sort(key=lambda x: x['time'])
|
|||
|
|
|
|||
|
|
# 过滤
|
|||
|
|
if len(klines) > limit:
|
|||
|
|
klines = klines[-limit:]
|
|||
|
|
|
|||
|
|
# logger.info(f"获取到 {len(klines)} 条 Tiingo 外汇数据")
|
|||
|
|
return klines
|
|||
|
|
|
|||
|
|
except requests.exceptions.RequestException as e:
|
|||
|
|
logger.error(f"Tiingo API request failed: {e}")
|
|||
|
|
return []
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.error(f"Failed to process Tiingo data: {e}")
|
|||
|
|
return []
|