replay system
This commit is contained in:
306
COBY/api/replay_api.py
Normal file
306
COBY/api/replay_api.py
Normal file
@ -0,0 +1,306 @@
|
||||
"""
|
||||
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
|
Reference in New Issue
Block a user