306 lines
12 KiB
Python
306 lines
12 KiB
Python
"""
|
|
REST API endpoints for historical data replay functionality.
|
|
"""
|
|
|
|
from fastapi import APIRouter, HTTPException, Query, Path
|
|
from typing import Optional, List, Dict, Any
|
|
from datetime import datetime
|
|
from pydantic import BaseModel, Field
|
|
|
|
from ..replay.replay_manager import HistoricalReplayManager
|
|
from ..models.core import ReplayStatus
|
|
from ..utils.logging import get_logger, set_correlation_id
|
|
from ..utils.exceptions import ReplayError, ValidationError
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
class CreateReplayRequest(BaseModel):
|
|
"""Request model for creating replay session"""
|
|
start_time: datetime = Field(..., description="Replay start time")
|
|
end_time: datetime = Field(..., description="Replay end time")
|
|
speed: float = Field(1.0, gt=0, le=100, description="Playback speed multiplier")
|
|
symbols: Optional[List[str]] = Field(None, description="Symbols to replay")
|
|
exchanges: Optional[List[str]] = Field(None, description="Exchanges to replay")
|
|
|
|
|
|
class ReplayControlRequest(BaseModel):
|
|
"""Request model for replay control operations"""
|
|
action: str = Field(..., description="Control action: start, pause, resume, stop")
|
|
|
|
|
|
class SeekRequest(BaseModel):
|
|
"""Request model for seeking in replay"""
|
|
timestamp: datetime = Field(..., description="Target timestamp")
|
|
|
|
|
|
class SpeedRequest(BaseModel):
|
|
"""Request model for changing replay speed"""
|
|
speed: float = Field(..., gt=0, le=100, description="New playback speed")
|
|
|
|
|
|
def create_replay_router(replay_manager: HistoricalReplayManager) -> APIRouter:
|
|
"""Create replay API router with endpoints"""
|
|
|
|
router = APIRouter(prefix="/replay", tags=["replay"])
|
|
|
|
@router.post("/sessions", response_model=Dict[str, str])
|
|
async def create_replay_session(request: CreateReplayRequest):
|
|
"""Create a new replay session"""
|
|
try:
|
|
set_correlation_id()
|
|
|
|
session_id = replay_manager.create_replay_session(
|
|
start_time=request.start_time,
|
|
end_time=request.end_time,
|
|
speed=request.speed,
|
|
symbols=request.symbols,
|
|
exchanges=request.exchanges
|
|
)
|
|
|
|
logger.info(f"Created replay session {session_id}")
|
|
|
|
return {
|
|
"session_id": session_id,
|
|
"status": "created",
|
|
"message": "Replay session created successfully"
|
|
}
|
|
|
|
except ValidationError as e:
|
|
logger.warning(f"Invalid replay request: {e}")
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except ReplayError as e:
|
|
logger.error(f"Replay creation failed: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error creating replay session: {e}")
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
@router.get("/sessions", response_model=List[Dict[str, Any]])
|
|
async def list_replay_sessions():
|
|
"""List all replay sessions"""
|
|
try:
|
|
sessions = replay_manager.list_replay_sessions()
|
|
|
|
return [
|
|
{
|
|
"session_id": session.session_id,
|
|
"start_time": session.start_time.isoformat(),
|
|
"end_time": session.end_time.isoformat(),
|
|
"current_time": session.current_time.isoformat(),
|
|
"speed": session.speed,
|
|
"status": session.status.value,
|
|
"symbols": session.symbols,
|
|
"exchanges": session.exchanges,
|
|
"progress": session.progress,
|
|
"events_replayed": session.events_replayed,
|
|
"total_events": session.total_events,
|
|
"created_at": session.created_at.isoformat(),
|
|
"started_at": session.started_at.isoformat() if session.started_at else None,
|
|
"error_message": getattr(session, 'error_message', None)
|
|
}
|
|
for session in sessions
|
|
]
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error listing replay sessions: {e}")
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
@router.get("/sessions/{session_id}", response_model=Dict[str, Any])
|
|
async def get_replay_session(session_id: str = Path(..., description="Session ID")):
|
|
"""Get replay session details"""
|
|
try:
|
|
session = replay_manager.get_replay_status(session_id)
|
|
|
|
if not session:
|
|
raise HTTPException(status_code=404, detail="Session not found")
|
|
|
|
return {
|
|
"session_id": session.session_id,
|
|
"start_time": session.start_time.isoformat(),
|
|
"end_time": session.end_time.isoformat(),
|
|
"current_time": session.current_time.isoformat(),
|
|
"speed": session.speed,
|
|
"status": session.status.value,
|
|
"symbols": session.symbols,
|
|
"exchanges": session.exchanges,
|
|
"progress": session.progress,
|
|
"events_replayed": session.events_replayed,
|
|
"total_events": session.total_events,
|
|
"created_at": session.created_at.isoformat(),
|
|
"started_at": session.started_at.isoformat() if session.started_at else None,
|
|
"paused_at": session.paused_at.isoformat() if session.paused_at else None,
|
|
"stopped_at": session.stopped_at.isoformat() if session.stopped_at else None,
|
|
"completed_at": session.completed_at.isoformat() if session.completed_at else None,
|
|
"error_message": getattr(session, 'error_message', None)
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error getting replay session {session_id}: {e}")
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
@router.post("/sessions/{session_id}/control", response_model=Dict[str, str])
|
|
async def control_replay_session(
|
|
session_id: str = Path(..., description="Session ID"),
|
|
request: ReplayControlRequest = None
|
|
):
|
|
"""Control replay session (start, pause, resume, stop)"""
|
|
try:
|
|
set_correlation_id()
|
|
|
|
if not request:
|
|
raise HTTPException(status_code=400, detail="Control action required")
|
|
|
|
action = request.action.lower()
|
|
|
|
if action == "start":
|
|
await replay_manager.start_replay(session_id)
|
|
message = "Replay started"
|
|
elif action == "pause":
|
|
await replay_manager.pause_replay(session_id)
|
|
message = "Replay paused"
|
|
elif action == "resume":
|
|
await replay_manager.resume_replay(session_id)
|
|
message = "Replay resumed"
|
|
elif action == "stop":
|
|
await replay_manager.stop_replay(session_id)
|
|
message = "Replay stopped"
|
|
else:
|
|
raise HTTPException(status_code=400, detail="Invalid action")
|
|
|
|
logger.info(f"Replay session {session_id} action: {action}")
|
|
|
|
return {
|
|
"session_id": session_id,
|
|
"action": action,
|
|
"message": message
|
|
}
|
|
|
|
except ReplayError as e:
|
|
logger.error(f"Replay control failed for {session_id}: {e}")
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error controlling replay {session_id}: {e}")
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
@router.post("/sessions/{session_id}/seek", response_model=Dict[str, str])
|
|
async def seek_replay_session(
|
|
session_id: str = Path(..., description="Session ID"),
|
|
request: SeekRequest = None
|
|
):
|
|
"""Seek to specific timestamp in replay"""
|
|
try:
|
|
if not request:
|
|
raise HTTPException(status_code=400, detail="Timestamp required")
|
|
|
|
success = replay_manager.seek_replay(session_id, request.timestamp)
|
|
|
|
if not success:
|
|
raise HTTPException(status_code=400, detail="Seek failed")
|
|
|
|
logger.info(f"Seeked replay session {session_id} to {request.timestamp}")
|
|
|
|
return {
|
|
"session_id": session_id,
|
|
"timestamp": request.timestamp.isoformat(),
|
|
"message": "Seek successful"
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error seeking replay session {session_id}: {e}")
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
@router.post("/sessions/{session_id}/speed", response_model=Dict[str, Any])
|
|
async def set_replay_speed(
|
|
session_id: str = Path(..., description="Session ID"),
|
|
request: SpeedRequest = None
|
|
):
|
|
"""Change replay speed"""
|
|
try:
|
|
if not request:
|
|
raise HTTPException(status_code=400, detail="Speed required")
|
|
|
|
success = replay_manager.set_replay_speed(session_id, request.speed)
|
|
|
|
if not success:
|
|
raise HTTPException(status_code=400, detail="Speed change failed")
|
|
|
|
logger.info(f"Set replay speed to {request.speed}x for session {session_id}")
|
|
|
|
return {
|
|
"session_id": session_id,
|
|
"speed": request.speed,
|
|
"message": "Speed changed successfully"
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error setting replay speed for {session_id}: {e}")
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
@router.delete("/sessions/{session_id}", response_model=Dict[str, str])
|
|
async def delete_replay_session(session_id: str = Path(..., description="Session ID")):
|
|
"""Delete replay session"""
|
|
try:
|
|
success = replay_manager.delete_replay_session(session_id)
|
|
|
|
if not success:
|
|
raise HTTPException(status_code=404, detail="Session not found")
|
|
|
|
logger.info(f"Deleted replay session {session_id}")
|
|
|
|
return {
|
|
"session_id": session_id,
|
|
"message": "Session deleted successfully"
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error deleting replay session {session_id}: {e}")
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
@router.get("/data-range/{symbol}", response_model=Dict[str, Any])
|
|
async def get_data_range(
|
|
symbol: str = Path(..., description="Trading symbol"),
|
|
exchange: Optional[str] = Query(None, description="Exchange name")
|
|
):
|
|
"""Get available data time range for a symbol"""
|
|
try:
|
|
data_range = await replay_manager.get_available_data_range(symbol, exchange)
|
|
|
|
if not data_range:
|
|
raise HTTPException(status_code=404, detail="No data available for symbol")
|
|
|
|
return {
|
|
"symbol": symbol,
|
|
"exchange": exchange,
|
|
"start_time": data_range['start'].isoformat(),
|
|
"end_time": data_range['end'].isoformat(),
|
|
"duration_days": (data_range['end'] - data_range['start']).days
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error getting data range for {symbol}: {e}")
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
@router.get("/stats", response_model=Dict[str, Any])
|
|
async def get_replay_stats():
|
|
"""Get replay system statistics"""
|
|
try:
|
|
return replay_manager.get_stats()
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting replay stats: {e}")
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
return router |