Spaces:
Runtime error
Runtime error
File size: 11,432 Bytes
63bad2b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 | import os
os.environ.setdefault("MPLBACKEND", "Agg")
from datetime import datetime, timedelta
from typing import Optional
import pandas as pd
import yfinance as yf
from fmp_python.fmp import FMP
from tqdm import tqdm
CACHE_DIR = "data_cache"
os.makedirs(CACHE_DIR, exist_ok=True)
def _get_cache_filename_yf(tckr_symbl, interval, start_date, end_date, adjust_prices):
start_str = start_date.replace("-", "_")
end_str = end_date.replace("-", "_")
adjust_str = "adj" if adjust_prices else "raw"
return os.path.join(CACHE_DIR, f"{tckr_symbl}_{interval}_{start_str}_to_{end_str}_{adjust_str}.csv")
def _get_cache_filename_fmp(tckr_symbl: str, interval: str) -> str:
return os.path.join(CACHE_DIR, f"{tckr_symbl}_{interval}_fmp.csv")
def _is_valid_cache(df: pd.DataFrame) -> bool:
if df.empty or len(df) < 2:
return False
if not isinstance(df.index, pd.DatetimeIndex):
return False
required_upper = ["Open", "High", "Low", "Close", "Volume"]
required_lower = ["open", "high", "low", "close", "volume"]
has_upper = all(col in df.columns for col in required_upper)
has_lower = all(col in df.columns for col in required_lower)
if not (has_upper or has_lower):
return False
return True
def _load_cached_data(cache_file: str) -> Optional[pd.DataFrame]:
if os.path.exists(cache_file):
try:
df = pd.read_csv(cache_file, index_col=0, parse_dates=True, header=0)
if isinstance(df.columns, pd.MultiIndex):
df.columns = df.columns.get_level_values(0)
if not isinstance(df.index, pd.DatetimeIndex):
try:
df.index = pd.to_datetime(df.index)
except Exception as e:
print(f"Could not parse dates in cache: {e}, will re-download")
return None
expected_upper = ["Open", "High", "Low", "Close", "Volume"]
expected_lower = ["open", "high", "low", "close", "volume"]
actual_cols = list(df.columns)
has_upper = any(col in actual_cols for col in expected_upper)
has_lower = any(col in actual_cols for col in expected_lower)
if not (has_upper or has_lower):
print(f"Unexpected columns in cache: {actual_cols}, will re-download")
return None
print(f"Loaded cached data: {len(df)} rows, columns: {list(df.columns)} from {cache_file}")
return df
except Exception as e:
print(f"Error loading cache: {e}, will re-download")
import traceback
traceback.print_exc()
return None
return None
def _save_cached_data(df: pd.DataFrame, cache_file: str):
try:
df.to_csv(cache_file)
print(f"Cached data saved: {cache_file}")
except Exception as e:
print(f"Error saving cache: {e}")
# FMP interval -> max days per API chunk
FMP_INTERVAL_DAYS = {"1min": 2, "5min": 7, "15min": 38, "1hour": 70, "4hour": 160}
def _fetch_fmp_range(fmp, tckr_symbl, interval, start_dt, end_dt, progress=None):
"""Download FMP data for a date range. Returns DataFrame with 'date' index."""
chunk_span = timedelta(days=FMP_INTERVAL_DAYS[interval])
frames = []
# Build chunk list
chunks = []
temp = start_dt
while temp <= end_dt:
chunks.append(temp)
temp = min(temp + chunk_span, end_dt) + timedelta(days=1)
# Download with progress
if progress:
chunk_iter = progress.tqdm(chunks, desc=f"FMP {interval}")
else:
chunk_iter = tqdm(chunks, desc=f"FMP {interval}")
for chunk_start in chunk_iter:
chunk_end = min(chunk_start + chunk_span, end_dt)
try:
chunk = fmp.get_historical_chart(
interval, tckr_symbl,
_from=chunk_start.strftime("%Y-%m-%d"),
_to=chunk_end.strftime("%Y-%m-%d")
)
except Exception as e:
raise ValueError(f"FMP download failed ({chunk_start.date()} to {chunk_end.date()}): {e}")
if chunk is not None and not chunk.empty:
chunk["date"] = pd.to_datetime(chunk["date"], errors="coerce")
chunk = chunk.dropna(subset=["date"])
if not chunk.empty:
frames.append(chunk)
if not frames:
return pd.DataFrame()
df = pd.concat(frames, ignore_index=True)
df["date"] = pd.to_datetime(df["date"], errors="coerce")
df = df.dropna(subset=["date"])
# Timezone handling
if not df.empty and df["date"].dt.tz is None:
df["date"] = df["date"].dt.tz_localize("America/New_York", nonexistent="shift_forward", ambiguous="NaT")
df["date"] = df["date"].dt.tz_convert("UTC").dt.tz_localize(None)
return df.sort_values("date").set_index("date")
def _download_data_fmp(tckr_symbl, interval, date, progress=None, replay=False, compression=None):
if interval not in FMP_INTERVAL_DAYS:
raise ValueError(f"Unsupported FMP interval '{interval}'")
end_dt = datetime.strptime(date["end"], "%Y-%m-%d")
start_dt = datetime.strptime(date["start"], "%Y-%m-%d")
# Clamp to 15-year limit
max_lookback = end_dt - timedelta(days=15 * 365)
if start_dt < max_lookback:
print(f"Warning: start date clamped to {max_lookback.date()} (15y limit).")
start_dt = max_lookback
fmp = FMP(output_format="pandas", write_to_file=False)
cache_file = _get_cache_filename_fmp(tckr_symbl, interval)
cached_df = _load_cached_data(cache_file)
if cached_df is not None and _is_valid_cache(cached_df):
cache_start = cached_df.index.min().date()
cache_end = cached_df.index.max().date()
frames = []
# Download past data if needed
if start_dt.date() < cache_start:
past_end = datetime.combine(cache_start - timedelta(days=1), datetime.min.time())
print(f"Fetching past data: {start_dt.date()} to {past_end.date()}")
chunk_past = _fetch_fmp_range(fmp, tckr_symbl, interval, start_dt, past_end, progress)
if not chunk_past.empty:
frames.append(chunk_past)
frames.append(cached_df)
# Download current data if needed
if end_dt.date() > cache_end:
current_start = datetime.combine(cache_end + timedelta(days=1), datetime.min.time())
print(f"Fetching current data: {current_start.date()} to {end_dt.date()}")
chunk_current = _fetch_fmp_range(fmp, tckr_symbl, interval, current_start, end_dt, progress)
if not chunk_current.empty:
frames.append(chunk_current)
# Merge, dedupe, sort, save
if len(frames) > 1:
df = pd.concat(frames).sort_index()
df = df[~df.index.duplicated(keep='last')]
_save_cached_data(df, cache_file)
else:
df = cached_df
print(f"Using cached data: {len(df)} rows (no download needed)")
else:
# No cache - download full range
print(f"No cache, downloading: {start_dt.date()} to {end_dt.date()}")
df = _fetch_fmp_range(fmp, tckr_symbl, interval, start_dt, end_dt, progress)
if not df.empty:
_save_cached_data(df, cache_file)
if df.empty:
return df
# Filter to user's requested range
df = df[(df.index >= pd.Timestamp(start_dt)) & (df.index <= pd.Timestamp(end_dt))]
return df
def _download_data_yf(tckr_symbl, interval, date, adjust_prices, auto_period=True, period="60d"):
try:
print("Interval: ", interval)
start_dt = datetime.strptime(date["start"], "%Y-%m-%d")
end_dt = datetime.strptime(date["end"], "%Y-%m-%d")
cache_file = _get_cache_filename_yf(tckr_symbl, interval, date["start"], date["end"], adjust_prices)
cached_df = _load_cached_data(cache_file)
if cached_df is not None and _is_valid_cache(cached_df):
df = cached_df
print(f"Using cached data: {len(df)} rows (no download needed)")
else:
print("No valid cache, downloading...")
if interval in ["1m", "2m", "5m", "15m", "30m", "60m", "1h"] and auto_period:
if interval in ["1m"]:
max_days = 7
elif interval in ["2m", "5m", "15m", "30m"]:
max_days = 60
else:
max_days = 730
desired_days = max(1, (end_dt - start_dt).days or 1)
clamped_days = min(desired_days, max_days)
period = f"{clamped_days}d"
df = yf.download(tckr_symbl, period=period, interval=interval, auto_adjust=adjust_prices)
print(f"Downloaded {interval} data for {period}")
else:
df = yf.download(tckr_symbl, start=date["start"], end=date["end"], interval=interval, auto_adjust=adjust_prices)
print(f"Downloaded data from {date['start']} to {date['end']} with {interval} interval")
if isinstance(df.columns, pd.MultiIndex):
df.columns = df.columns.get_level_values(0)
_save_cached_data(df, cache_file)
if isinstance(df.columns, pd.MultiIndex):
df.columns = df.columns.get_level_values(0)
if df.empty:
raise ValueError("No data available for the specified parameters!")
if df.index.tz is not None:
df.index = df.index.tz_localize(None)
print(f"Data points: {len(df)}")
return df
except Exception as e:
raise ValueError(f"Error downloading data: {e}")
def get_data(
data_source: str,
tckr_symbl: str,
interval: str,
date: dict,
adjust_prices: bool = True,
auto_period: bool = True,
period: str = "60d",
upload_data: bool = False,
upload_data_path: str = None,
progress=None,
):
if upload_data:
if not upload_data_path:
raise ValueError("upload_data_path is required when upload_data is True.")
df = pd.read_csv(upload_data_path)
if df.shape[1] >= 2:
dt = pd.to_datetime(df.iloc[:, 0].astype(str) + " " + df.iloc[:, 1].astype(str), errors="coerce")
df = df.drop(columns=df.columns[:2])
else:
dt = pd.to_datetime(df.iloc[:, 0], errors="coerce")
df = df.drop(columns=df.columns[:1])
df.insert(0, "datetime", dt)
df = df.dropna(subset=["datetime"]).set_index("datetime")
expected_cols = ["open", "high", "low", "close", "volume"]
if len(df.columns) >= 5:
df.columns = list(expected_cols) + list(df.columns[len(expected_cols) :])
df.columns = [c.capitalize() for c in df.columns]
df.index = df.index.tz_localize(None)
return df
source = (data_source or "").lower()
loaders = {
"yahoofinance": lambda: _download_data_yf(tckr_symbl, interval, date, adjust_prices, auto_period, period),
"yf": lambda: _download_data_yf(tckr_symbl, interval, date, adjust_prices, auto_period, period),
"yahoo": lambda: _download_data_yf(tckr_symbl, interval, date, adjust_prices, auto_period, period),
"fmp": lambda: _download_data_fmp(tckr_symbl, interval, date, progress),
}
if source not in loaders:
raise ValueError(f"Invalid data source: {data_source}")
return loaders[source]()
|