Anurag33Gaikwad commited on
Commit
64b1b6e
·
verified ·
1 Parent(s): 92b9b86

Update app/services/market_data.py

Browse files
Files changed (1) hide show
  1. app/services/market_data.py +88 -76
app/services/market_data.py CHANGED
@@ -11,15 +11,59 @@ settings = get_settings()
11
 
12
  ALPHA_VANTAGE_URL = "https://www.alphavantage.co/query"
13
 
14
- _CACHE = {}
15
- _CACHE_TTL = 60 # seconds
16
 
17
 
18
  # -------------------------
19
  # Helpers
20
  # -------------------------
21
  def is_crypto(ticker: str) -> bool:
22
- return "-" in ticker or ticker.upper() in {"BTC", "ETH", "SOL", "BNB"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
 
25
  # -------------------------
@@ -30,26 +74,20 @@ def _fetch_stock_history(ticker: str) -> pd.DataFrame:
30
  "function": "TIME_SERIES_DAILY",
31
  "symbol": ticker,
32
  "apikey": settings.alpha_vantage_api_key,
33
- "outputsize": "compact",
34
  }
35
 
36
- response = requests.get(ALPHA_VANTAGE_URL, params=params, timeout=10)
37
- data = response.json()
38
 
39
  if "Time Series (Daily)" not in data:
40
  if "Note" in data or "Information" in data:
41
- raise HTTPException(
42
- status_code=429,
43
- detail="Alpha Vantage rate limit exceeded",
44
- )
45
- raise HTTPException(
46
- status_code=400,
47
- detail="Invalid stock ticker symbol",
48
- )
49
 
50
- ts = data["Time Series (Daily)"]
 
 
51
 
52
- df = pd.DataFrame.from_dict(ts, orient="index")
53
  df.rename(
54
  columns={
55
  "1. open": "Open",
@@ -62,17 +100,13 @@ def _fetch_stock_history(ticker: str) -> pd.DataFrame:
62
  )
63
 
64
  df = df[["Open", "High", "Low", "Close", "Volume"]]
65
- df = df.apply(pd.to_numeric, errors="coerce")
66
- df.dropna(inplace=True)
67
 
68
  df.index = pd.to_datetime(df.index)
69
  df.sort_index(inplace=True)
70
 
71
  if len(df) < settings.history_window + 50:
72
- raise HTTPException(
73
- status_code=400,
74
- detail="Not enough historical stock data",
75
- )
76
 
77
  return df
78
 
@@ -81,89 +115,58 @@ def _fetch_stock_history(ticker: str) -> pd.DataFrame:
81
  # CRYPTO DATA
82
  # -------------------------
83
  def _fetch_crypto_history(ticker: str) -> pd.DataFrame:
84
- # Normalize ticker
85
  if "-" in ticker:
86
- symbol, market = ticker.split("-")
87
  else:
88
- symbol, market = ticker.upper(), "USD"
89
 
90
  params = {
91
  "function": "DIGITAL_CURRENCY_DAILY",
92
- "symbol": symbol,
93
- "market": market,
94
  "apikey": settings.alpha_vantage_api_key,
95
  }
96
 
97
- response = requests.get(ALPHA_VANTAGE_URL, params=params, timeout=10)
98
- data = response.json()
99
 
100
  if "Time Series (Digital Currency Daily)" not in data:
101
  if "Note" in data or "Information" in data:
102
- raise HTTPException(
103
- status_code=429,
104
- detail="Alpha Vantage rate limit exceeded",
105
- )
106
- raise HTTPException(
107
- status_code=400,
108
- detail="Invalid crypto ticker symbol",
109
- )
110
 
111
- ts = data["Time Series (Digital Currency Daily)"]
112
-
113
- df = pd.DataFrame.from_dict(ts, orient="index")
114
-
115
- # ---- 🔑 Dynamic USD column detection (CRITICAL FIX) ----
116
- try:
117
- open_col = next(c for c in df.columns if "open (USD)" in c)
118
- high_col = next(c for c in df.columns if "high (USD)" in c)
119
- low_col = next(c for c in df.columns if "low (USD)" in c)
120
- close_col = next(c for c in df.columns if "close (USD)" in c)
121
- except StopIteration:
122
- raise HTTPException(
123
- status_code=500,
124
- detail="Unexpected crypto data format from Alpha Vantage",
125
- )
126
-
127
- volume_col = "5. volume"
128
-
129
- df = df[[open_col, high_col, low_col, close_col, volume_col]]
130
 
131
- df.columns = ["Open", "High", "Low", "Close", "Volume"]
132
 
133
- # ---- Clean & normalize ----
134
  df = df.apply(pd.to_numeric, errors="coerce")
135
  df.dropna(subset=["Open", "High", "Low", "Close"], inplace=True)
136
 
137
  df.index = pd.to_datetime(df.index)
138
  df.sort_index(inplace=True)
139
 
140
- # ---- Indicator safety buffer ----
141
  if len(df) < settings.history_window + 50:
142
- raise HTTPException(
143
- status_code=400,
144
- detail="Not enough historical crypto data after cleaning",
145
- )
146
 
147
  return df
148
 
149
 
150
-
151
  # -------------------------
152
  # Cached unified fetcher
153
  # -------------------------
154
  def fetch_raw_history(ticker: str) -> pd.DataFrame:
155
- now = time.time()
156
  key = ticker.upper()
 
157
 
158
- if key in _CACHE:
159
- cached = _CACHE[key]
160
- if now - cached["ts"] < _CACHE_TTL:
161
- return cached["df"]
162
 
163
- if is_crypto(key):
164
- df = _fetch_crypto_history(key)
165
- else:
166
- df = _fetch_stock_history(key)
 
167
 
168
  _CACHE[key] = {"df": df, "ts": now}
169
  return df
@@ -175,12 +178,21 @@ def fetch_raw_history(ticker: str) -> pd.DataFrame:
175
  def get_enriched_history(ticker: str) -> Tuple[pd.DataFrame, pd.DataFrame]:
176
  df_raw = fetch_raw_history(ticker)
177
  df_tech = add_technical_indicators(df_raw)
 
 
 
 
 
 
 
178
  return df_raw, df_tech
179
 
180
 
181
  def last_n_candles(df: pd.DataFrame, n: int) -> list[dict]:
182
- tail = df.tail(n)
183
  return [
184
- {"date": idx.strftime("%Y-%m-%d"), "price": float(row["Close"])}
185
- for idx, row in tail.iterrows()
 
 
 
186
  ]
 
11
 
12
  ALPHA_VANTAGE_URL = "https://www.alphavantage.co/query"
13
 
14
+ _CACHE: dict[str, dict] = {}
15
+ _CACHE_TTL = 60 # seconds (safe for AV free tier)
16
 
17
 
18
  # -------------------------
19
  # Helpers
20
  # -------------------------
21
  def is_crypto(ticker: str) -> bool:
22
+ t = ticker.upper()
23
+ return "-" in t or t in {"BTC", "ETH", "SOL", "BNB", "XRP", "DOGE"}
24
+
25
+
26
+ def _safe_get_json(params: dict) -> dict:
27
+ try:
28
+ resp = requests.get(ALPHA_VANTAGE_URL, params=params, timeout=10)
29
+ return resp.json()
30
+ except Exception:
31
+ raise HTTPException(
32
+ status_code=503,
33
+ detail="Market data provider unavailable",
34
+ )
35
+
36
+
37
+ def _detect_ohlcv_columns(df: pd.DataFrame) -> pd.DataFrame:
38
+ """
39
+ Robust OHLCV resolver for Alpha Vantage crypto.
40
+ Works for:
41
+ - 1a / 1b
42
+ - USD / non-USD
43
+ - future schema changes
44
+ """
45
+ def find(keywords):
46
+ for c in df.columns:
47
+ name = c.lower()
48
+ if all(k in name for k in keywords):
49
+ return c
50
+ return None
51
+
52
+ open_col = find(["open"])
53
+ high_col = find(["high"])
54
+ low_col = find(["low"])
55
+ close_col = find(["close"])
56
+ volume_col = find(["volume"])
57
+
58
+ if not all([open_col, high_col, low_col, close_col, volume_col]):
59
+ raise HTTPException(
60
+ status_code=500,
61
+ detail=f"Unsupported crypto data format from Alpha Vantage: {list(df.columns)}",
62
+ )
63
+
64
+ df = df[[open_col, high_col, low_col, close_col, volume_col]]
65
+ df.columns = ["Open", "High", "Low", "Close", "Volume"]
66
+ return df
67
 
68
 
69
  # -------------------------
 
74
  "function": "TIME_SERIES_DAILY",
75
  "symbol": ticker,
76
  "apikey": settings.alpha_vantage_api_key,
77
+ "outputsize": "compact", # FREE tier only
78
  }
79
 
80
+ data = _safe_get_json(params)
 
81
 
82
  if "Time Series (Daily)" not in data:
83
  if "Note" in data or "Information" in data:
84
+ raise HTTPException(429, "Alpha Vantage rate limit exceeded")
85
+ raise HTTPException(400, "Invalid stock ticker symbol")
 
 
 
 
 
 
86
 
87
+ df = pd.DataFrame.from_dict(
88
+ data["Time Series (Daily)"], orient="index"
89
+ )
90
 
 
91
  df.rename(
92
  columns={
93
  "1. open": "Open",
 
100
  )
101
 
102
  df = df[["Open", "High", "Low", "Close", "Volume"]]
103
+ df = df.apply(pd.to_numeric, errors="coerce").dropna()
 
104
 
105
  df.index = pd.to_datetime(df.index)
106
  df.sort_index(inplace=True)
107
 
108
  if len(df) < settings.history_window + 50:
109
+ raise HTTPException(400, "Not enough historical stock data")
 
 
 
110
 
111
  return df
112
 
 
115
  # CRYPTO DATA
116
  # -------------------------
117
  def _fetch_crypto_history(ticker: str) -> pd.DataFrame:
 
118
  if "-" in ticker:
119
+ symbol, market = ticker.split("-", 1)
120
  else:
121
+ symbol, market = ticker, "USD"
122
 
123
  params = {
124
  "function": "DIGITAL_CURRENCY_DAILY",
125
+ "symbol": symbol.upper(),
126
+ "market": market.upper(),
127
  "apikey": settings.alpha_vantage_api_key,
128
  }
129
 
130
+ data = _safe_get_json(params)
 
131
 
132
  if "Time Series (Digital Currency Daily)" not in data:
133
  if "Note" in data or "Information" in data:
134
+ raise HTTPException(429, "Alpha Vantage rate limit exceeded")
135
+ raise HTTPException(400, "Invalid crypto ticker symbol")
 
 
 
 
 
 
136
 
137
+ df = pd.DataFrame.from_dict(
138
+ data["Time Series (Digital Currency Daily)"], orient="index"
139
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
 
141
+ df = _detect_ohlcv_columns(df)
142
 
 
143
  df = df.apply(pd.to_numeric, errors="coerce")
144
  df.dropna(subset=["Open", "High", "Low", "Close"], inplace=True)
145
 
146
  df.index = pd.to_datetime(df.index)
147
  df.sort_index(inplace=True)
148
 
 
149
  if len(df) < settings.history_window + 50:
150
+ raise HTTPException(400, "Not enough historical crypto data")
 
 
 
151
 
152
  return df
153
 
154
 
 
155
  # -------------------------
156
  # Cached unified fetcher
157
  # -------------------------
158
  def fetch_raw_history(ticker: str) -> pd.DataFrame:
 
159
  key = ticker.upper()
160
+ now = time.time()
161
 
162
+ if key in _CACHE and now - _CACHE[key]["ts"] < _CACHE_TTL:
163
+ return _CACHE[key]["df"]
 
 
164
 
165
+ df = (
166
+ _fetch_crypto_history(key)
167
+ if is_crypto(key)
168
+ else _fetch_stock_history(key)
169
+ )
170
 
171
  _CACHE[key] = {"df": df, "ts": now}
172
  return df
 
178
  def get_enriched_history(ticker: str) -> Tuple[pd.DataFrame, pd.DataFrame]:
179
  df_raw = fetch_raw_history(ticker)
180
  df_tech = add_technical_indicators(df_raw)
181
+
182
+ if len(df_tech) < settings.history_window:
183
+ raise HTTPException(
184
+ status_code=400,
185
+ detail="Insufficient data after technical indicator calculation",
186
+ )
187
+
188
  return df_raw, df_tech
189
 
190
 
191
  def last_n_candles(df: pd.DataFrame, n: int) -> list[dict]:
 
192
  return [
193
+ {
194
+ "date": idx.strftime("%Y-%m-%d"),
195
+ "price": float(row["Close"]),
196
+ }
197
+ for idx, row in df.tail(n).iterrows()
198
  ]