Spaces:
Sleeping
Major fix: OANDA REST API direct integration (no external libraries)
Browse filesβ
Complete architecture change:
- Replaced oandapyV20 with direct HTTP REST API calls
- Uses only requests library (already available)
- More reliable in Hugging Face environment
- Better error handling with HTTP status codes
- Enhanced debugging with detailed error messages
π§ Technical improvements:
- Direct API calls to OANDA v3 REST endpoints
- Enhanced symbol mapping and validation
- Better authentication handling
- Detailed error reporting (401, 403, 404)
- Improved candle parsing and DataFrame creation
π― Goal: Finally resolve "Insufficient data available" error
- Uses battle-tested HTTP requests instead of complex libraries
- Simplified dependency chain
- Maximum compatibility with Hugging Face Spaces
π€ Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- app.py +68 -61
- requirements.txt +1 -2
|
@@ -21,16 +21,9 @@ OANDA_API_KEY = "5c973356b8d3d02c28162fd3f3afe8aa-1416e27af60c960fedb8d61c98e917
|
|
| 21 |
OANDA_ACCOUNT_ID = "101-004-37254450-002"
|
| 22 |
OANDA_BASE_URL = "https://api-fxpractice.oanda.com"
|
| 23 |
|
| 24 |
-
#
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
import oandapyV20.endpoints.pricing as pricing
|
| 28 |
-
import oandapyV20.endpoints.instruments as instruments
|
| 29 |
-
OANDA_AVAILABLE = True
|
| 30 |
-
print("β
OANDA API available")
|
| 31 |
-
except ImportError as e:
|
| 32 |
-
OANDA_AVAILABLE = False
|
| 33 |
-
print(f"β OANDA API not available: {e}")
|
| 34 |
|
| 35 |
# Try to import TimesFM
|
| 36 |
try:
|
|
@@ -285,7 +278,7 @@ def get_yahoo_data_extended(symbol: str) -> pd.DataFrame:
|
|
| 285 |
return None
|
| 286 |
|
| 287 |
def get_oanda_data(symbol: str) -> pd.DataFrame:
|
| 288 |
-
"""Get OANDA historical data
|
| 289 |
if not OANDA_AVAILABLE:
|
| 290 |
print("β OANDA API not available")
|
| 291 |
return None
|
|
@@ -312,7 +305,13 @@ def get_oanda_data(symbol: str) -> pd.DataFrame:
|
|
| 312 |
}
|
| 313 |
|
| 314 |
instrument = symbol_mapping.get(oanda_symbol, oanda_symbol)
|
| 315 |
-
print(f"π¦ Fetching OANDA data - Original: {symbol} -> Mapped: {instrument}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
|
| 317 |
# Prioritize granularities for better historical coverage
|
| 318 |
granularities = [
|
|
@@ -326,82 +325,90 @@ def get_oanda_data(symbol: str) -> pd.DataFrame:
|
|
| 326 |
|
| 327 |
for granularity in granularities:
|
| 328 |
try:
|
| 329 |
-
print(f"π Trying OANDA {granularity} candles for {instrument}")
|
| 330 |
|
| 331 |
-
#
|
| 332 |
-
|
| 333 |
-
access_token=OANDA_API_KEY,
|
| 334 |
-
environment="practice"
|
| 335 |
-
)
|
| 336 |
-
|
| 337 |
-
# Get historical candles
|
| 338 |
params = {
|
| 339 |
"granularity": granularity,
|
| 340 |
"count": 200, # Get maximum candles for analysis
|
| 341 |
"price": "MBA" # Mid, Bid, Ask prices
|
| 342 |
}
|
| 343 |
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
)
|
| 349 |
|
| 350 |
-
|
|
|
|
| 351 |
|
| 352 |
-
|
| 353 |
-
|
| 354 |
|
| 355 |
-
|
| 356 |
-
|
| 357 |
|
| 358 |
-
|
| 359 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 367 |
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
"High": float(candle['mid']['h']),
|
| 372 |
-
"Low": float(candle['mid']['l']),
|
| 373 |
-
"Close": float(candle['mid']['c']),
|
| 374 |
-
"Volume": int(candle.get('volume', 1000))
|
| 375 |
-
})
|
| 376 |
-
except Exception as candle_error:
|
| 377 |
-
print(f"β οΈ Error parsing candle: {candle_error}")
|
| 378 |
-
continue
|
| 379 |
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
df.set_index("Date", inplace=True)
|
| 383 |
|
| 384 |
-
|
| 385 |
-
|
| 386 |
|
| 387 |
-
|
| 388 |
-
|
| 389 |
|
| 390 |
else:
|
| 391 |
-
print(f"
|
| 392 |
|
| 393 |
else:
|
| 394 |
-
print(f"β
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
|
| 396 |
except Exception as e:
|
| 397 |
-
print(f"β OANDA error with {granularity}: {e}")
|
| 398 |
continue
|
| 399 |
|
| 400 |
-
print(f"β All OANDA granularities failed for {instrument}")
|
| 401 |
return None
|
| 402 |
|
| 403 |
except Exception as e:
|
| 404 |
-
print(f"β OANDA
|
| 405 |
return None
|
| 406 |
|
| 407 |
# Cache for performance
|
|
|
|
| 21 |
OANDA_ACCOUNT_ID = "101-004-37254450-002"
|
| 22 |
OANDA_BASE_URL = "https://api-fxpractice.oanda.com"
|
| 23 |
|
| 24 |
+
# OANDA REST API - Direct HTTP requests (more reliable)
|
| 25 |
+
OANDA_AVAILABLE = True # Always available with requests
|
| 26 |
+
print("β
OANDA REST API available (direct HTTP)")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
# Try to import TimesFM
|
| 29 |
try:
|
|
|
|
| 278 |
return None
|
| 279 |
|
| 280 |
def get_oanda_data(symbol: str) -> pd.DataFrame:
|
| 281 |
+
"""Get OANDA historical data via REST API - Independent from Supabase"""
|
| 282 |
if not OANDA_AVAILABLE:
|
| 283 |
print("β OANDA API not available")
|
| 284 |
return None
|
|
|
|
| 305 |
}
|
| 306 |
|
| 307 |
instrument = symbol_mapping.get(oanda_symbol, oanda_symbol)
|
| 308 |
+
print(f"π¦ Fetching OANDA data via REST - Original: {symbol} -> Mapped: {instrument}")
|
| 309 |
+
|
| 310 |
+
# Set up headers for OANDA API
|
| 311 |
+
headers = {
|
| 312 |
+
'Authorization': f'Bearer {OANDA_API_KEY}',
|
| 313 |
+
'Content-Type': 'application/json'
|
| 314 |
+
}
|
| 315 |
|
| 316 |
# Prioritize granularities for better historical coverage
|
| 317 |
granularities = [
|
|
|
|
| 325 |
|
| 326 |
for granularity in granularities:
|
| 327 |
try:
|
| 328 |
+
print(f"π Trying OANDA {granularity} candles via REST for {instrument}")
|
| 329 |
|
| 330 |
+
# Build OANDA REST API URL
|
| 331 |
+
url = f"{OANDA_BASE_URL}/v3/instruments/{instrument}/candles"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 332 |
params = {
|
| 333 |
"granularity": granularity,
|
| 334 |
"count": 200, # Get maximum candles for analysis
|
| 335 |
"price": "MBA" # Mid, Bid, Ask prices
|
| 336 |
}
|
| 337 |
|
| 338 |
+
response = requests.get(url, headers=headers, params=params, timeout=30)
|
| 339 |
+
|
| 340 |
+
if response.status_code == 200:
|
| 341 |
+
data = response.json()
|
|
|
|
| 342 |
|
| 343 |
+
if 'candles' in data and data['candles']:
|
| 344 |
+
candles = data['candles']
|
| 345 |
|
| 346 |
+
# Filter only complete candles (no incomplete data)
|
| 347 |
+
complete_candles = [c for c in candles if c.get('complete', True)]
|
| 348 |
|
| 349 |
+
if len(complete_candles) >= 50:
|
| 350 |
+
print(f"β
OANDA REST historical candles received: {len(complete_candles)} complete candles")
|
| 351 |
|
| 352 |
+
# Convert candles to DataFrame
|
| 353 |
+
df_data = []
|
| 354 |
+
for candle in complete_candles:
|
| 355 |
+
try:
|
| 356 |
+
# Parse candle timestamp
|
| 357 |
+
candle_time = datetime.fromisoformat(candle['time'].replace('Z', '+00:00'))
|
| 358 |
|
| 359 |
+
df_data.append({
|
| 360 |
+
"Date": candle_time,
|
| 361 |
+
"Open": float(candle['mid']['o']),
|
| 362 |
+
"High": float(candle['mid']['h']),
|
| 363 |
+
"Low": float(candle['mid']['l']),
|
| 364 |
+
"Close": float(candle['mid']['c']),
|
| 365 |
+
"Volume": int(candle.get('volume', 1000))
|
| 366 |
+
})
|
| 367 |
+
except Exception as candle_error:
|
| 368 |
+
print(f"β οΈ Error parsing candle: {candle_error}")
|
| 369 |
+
continue
|
| 370 |
|
| 371 |
+
if df_data:
|
| 372 |
+
df = pd.DataFrame(df_data)
|
| 373 |
+
df.set_index("Date", inplace=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
|
| 375 |
+
# Sort by date (oldest first for time series)
|
| 376 |
+
df.sort_index(inplace=True)
|
|
|
|
| 377 |
|
| 378 |
+
print(f"β
OANDA REST DataFrame created: {len(df)} data points from {granularity}")
|
| 379 |
+
return df
|
| 380 |
|
| 381 |
+
else:
|
| 382 |
+
print(f"β οΈ Only {len(complete_candles)} complete candles, need 50+")
|
| 383 |
|
| 384 |
else:
|
| 385 |
+
print(f"β No candles in OANDA REST response for {instrument}")
|
| 386 |
|
| 387 |
else:
|
| 388 |
+
print(f"β OANDA REST API failed with status {response.status_code}")
|
| 389 |
+
if response.status_code == 401:
|
| 390 |
+
print("β Authentication failed - check OANDA API key")
|
| 391 |
+
elif response.status_code == 404:
|
| 392 |
+
print(f"β Instrument {instrument} not found")
|
| 393 |
+
elif response.status_code == 403:
|
| 394 |
+
print("β Forbidden - check account permissions")
|
| 395 |
+
|
| 396 |
+
# Print error details for debugging
|
| 397 |
+
try:
|
| 398 |
+
error_data = response.json()
|
| 399 |
+
print(f"β OANDA API error: {error_data}")
|
| 400 |
+
except:
|
| 401 |
+
print(f"β OANDA API response: {response.text}")
|
| 402 |
|
| 403 |
except Exception as e:
|
| 404 |
+
print(f"β OANDA REST error with {granularity}: {e}")
|
| 405 |
continue
|
| 406 |
|
| 407 |
+
print(f"β All OANDA REST granularities failed for {instrument}")
|
| 408 |
return None
|
| 409 |
|
| 410 |
except Exception as e:
|
| 411 |
+
print(f"β OANDA REST connection error: {e}")
|
| 412 |
return None
|
| 413 |
|
| 414 |
# Cache for performance
|
|
@@ -30,6 +30,5 @@ huggingface-hub>=0.17.0
|
|
| 30 |
urllib3==1.26.18
|
| 31 |
scikit-learn>=1.3.0
|
| 32 |
|
| 33 |
-
# OANDA API integration
|
| 34 |
-
oandapyV20==0.7.2
|
| 35 |
requests-cache==1.1.0
|
|
|
|
| 30 |
urllib3==1.26.18
|
| 31 |
scikit-learn>=1.3.0
|
| 32 |
|
| 33 |
+
# OANDA API integration (via REST - no external dependencies)
|
|
|
|
| 34 |
requests-cache==1.1.0
|