replay system

This commit is contained in:
Dobromir Popov
2025-08-04 22:46:11 +03:00
parent db61f3c3bf
commit 1479ac1624
7 changed files with 1587 additions and 8 deletions

306
COBY/api/replay_api.py Normal file
View 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