""" Data Synchronization API Routes OrbiQuant IA Trading Platform - Data Service Endpoints for managing data synchronization with Massive.com/Polygon.io """ from datetime import datetime from typing import Optional, List, Dict, Any from fastapi import APIRouter, Depends, HTTPException, Query, status, BackgroundTasks from pydantic import BaseModel, Field import asyncpg from providers.polygon_client import AssetType, Timeframe from services.sync_service import DataSyncService, SyncStatus from .dependencies import get_db_pool, get_polygon_client router = APIRouter(prefix="/api/sync", tags=["Data Sync"]) # ============================================================================= # Request/Response Models # ============================================================================= class SyncSymbolRequest(BaseModel): """Request to sync a specific symbol.""" asset_type: AssetType = Field(AssetType.FOREX, description="Asset type") timeframe: Timeframe = Field(Timeframe.MINUTE_5, description="Timeframe to sync") backfill_days: int = Field(30, ge=1, le=365, description="Days to backfill") class SyncSymbolResponse(BaseModel): """Response from sync operation.""" status: str symbol: str timeframe: str rows_inserted: int start_date: Optional[str] = None end_date: Optional[str] = None error: Optional[str] = None class SyncStatusResponse(BaseModel): """Sync status for a ticker.""" symbol: str asset_type: str timeframe: Optional[str] = None last_sync: Optional[str] = None rows_synced: Optional[int] = None status: Optional[str] = None error: Optional[str] = None updated_at: Optional[str] = None class SyncAllResponse(BaseModel): """Response from syncing all tickers.""" total_tickers: int successful: int failed: int total_rows_inserted: int message: str class SymbolInfo(BaseModel): """Information about a supported symbol.""" symbol: str polygon_symbol: str mt4_symbol: Optional[str] = None asset_type: str pip_value: Optional[float] = None supported: bool = True class SymbolsListResponse(BaseModel): """List of supported symbols.""" symbols: List[SymbolInfo] total: int asset_types: List[str] # ============================================================================= # Dependency Functions # ============================================================================= async def get_sync_service( db_pool: asyncpg.Pool = Depends(get_db_pool), polygon_client = Depends(get_polygon_client) ) -> DataSyncService: """Get DataSyncService instance.""" return DataSyncService( polygon_client=polygon_client, db_pool=db_pool, batch_size=10000 ) # ============================================================================= # Symbols Endpoints # ============================================================================= @router.get("/symbols", response_model=SymbolsListResponse) async def list_supported_symbols( asset_type: Optional[AssetType] = Query(None, description="Filter by asset type"), sync_service: DataSyncService = Depends(get_sync_service) ): """ Get list of symbols supported by Massive.com/Polygon.io. Returns all symbols configured in the system with their mappings. Can be filtered by asset type (forex, crypto, index, stock). """ symbols = await sync_service.get_supported_symbols(asset_type=asset_type) # Get unique asset types asset_types = list(set(s["asset_type"] for s in symbols)) return SymbolsListResponse( symbols=[SymbolInfo(**s) for s in symbols], total=len(symbols), asset_types=sorted(asset_types) ) @router.get("/symbols/{symbol}") async def get_symbol_info( symbol: str, sync_service: DataSyncService = Depends(get_sync_service) ): """ Get detailed information about a specific symbol. Includes sync status and configuration. """ # Get symbol from supported list symbols = await sync_service.get_supported_symbols() symbol_info = next((s for s in symbols if s["symbol"].upper() == symbol.upper()), None) if not symbol_info: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Symbol {symbol} not supported" ) # Get sync status sync_status = await sync_service.get_sync_status(symbol=symbol) return { **symbol_info, "sync_status": sync_status } # ============================================================================= # Sync Control Endpoints # ============================================================================= @router.post("/sync/{symbol}", response_model=SyncSymbolResponse) async def sync_symbol( symbol: str, request: SyncSymbolRequest, background_tasks: BackgroundTasks, sync_service: DataSyncService = Depends(get_sync_service) ): """ Trigger data synchronization for a specific symbol. This will fetch historical data from Massive.com/Polygon.io and store it in the database. The operation runs in the background and returns immediately. Parameters: - **symbol**: Ticker symbol (e.g., 'EURUSD', 'BTCUSD') - **asset_type**: Type of asset (forex, crypto, index, stock) - **timeframe**: Data timeframe (1m, 5m, 15m, 1h, 4h, 1d) - **backfill_days**: Number of days to backfill (1-365) """ # Validate symbol is supported symbols = await sync_service.get_supported_symbols() if not any(s["symbol"].upper() == symbol.upper() for s in symbols): raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Symbol {symbol} not supported. Use /api/sync/symbols to see available symbols." ) # Start sync in background result = await sync_service.sync_ticker_data( symbol=symbol.upper(), asset_type=request.asset_type, timeframe=request.timeframe, backfill_days=request.backfill_days ) return SyncSymbolResponse(**result) @router.post("/sync-all", response_model=SyncAllResponse) async def sync_all_symbols( background_tasks: BackgroundTasks, timeframe: Timeframe = Query(Timeframe.MINUTE_5, description="Timeframe to sync"), backfill_days: int = Query(1, ge=1, le=30, description="Days to backfill"), sync_service: DataSyncService = Depends(get_sync_service) ): """ Trigger synchronization for all active tickers. This is a heavy operation and may take a while depending on the number of active tickers and the API rate limits. Only use this for initial setup or manual full sync. """ # Run sync in background def run_sync(): import asyncio loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) result = loop.run_until_complete( sync_service.sync_all_active_tickers( timeframe=timeframe, backfill_days=backfill_days ) ) loop.close() return result background_tasks.add_task(run_sync) return SyncAllResponse( total_tickers=0, successful=0, failed=0, total_rows_inserted=0, message="Sync started in background. Check /api/sync/status for progress." ) # ============================================================================= # Status Endpoints # ============================================================================= @router.get("/status", response_model=List[SyncStatusResponse]) async def get_sync_status( symbol: Optional[str] = Query(None, description="Filter by symbol"), sync_service: DataSyncService = Depends(get_sync_service) ): """ Get synchronization status for all tickers or a specific symbol. Shows: - Last sync timestamp - Number of rows synced - Sync status (success, failed, in_progress) - Error messages if any """ status_list = await sync_service.get_sync_status(symbol=symbol) return [SyncStatusResponse(**s) for s in status_list] @router.get("/status/{symbol}", response_model=List[SyncStatusResponse]) async def get_symbol_sync_status( symbol: str, sync_service: DataSyncService = Depends(get_sync_service) ): """ Get detailed sync status for a specific symbol across all timeframes. """ status_list = await sync_service.get_sync_status(symbol=symbol) if not status_list: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"No sync status found for symbol {symbol}" ) return [SyncStatusResponse(**s) for s in status_list] # ============================================================================= # Health Check # ============================================================================= @router.get("/health") async def sync_health_check( polygon_client = Depends(get_polygon_client), db_pool: asyncpg.Pool = Depends(get_db_pool) ): """ Check health of sync service and data providers. Verifies: - Database connectivity - Polygon/Massive API accessibility - Rate limit status """ health = { "status": "healthy", "timestamp": datetime.utcnow().isoformat(), "providers": {} } # Check database try: async with db_pool.acquire() as conn: await conn.fetchval("SELECT 1") health["providers"]["database"] = { "status": "connected", "type": "PostgreSQL" } except Exception as e: health["status"] = "unhealthy" health["providers"]["database"] = { "status": "error", "error": str(e) } # Check Polygon API (basic connectivity) try: health["providers"]["polygon"] = { "status": "configured", "base_url": polygon_client.base_url, "rate_limit": f"{polygon_client.rate_limit} req/min" } except Exception as e: health["status"] = "degraded" health["providers"]["polygon"] = { "status": "error", "error": str(e) } return health