Files
crypto_trader/backend/api/autopilot.py

565 lines
19 KiB
Python
Raw Normal View History

"""AutoPilot API endpoints."""
from typing import Dict, Any, Optional, List
from fastapi import APIRouter, HTTPException, BackgroundTasks
from pydantic import BaseModel
from ..core.dependencies import get_database
from ..core.schemas import OrderSide
from src.core.database import get_database as get_db
# Import autopilot - path should be set up in main.py
from src.autopilot import (
stop_all_autopilots,
get_intelligent_autopilot,
get_strategy_selector,
get_performance_tracker,
get_performance_tracker,
get_autopilot_mode_info,
)
from src.worker.tasks import train_model_task
from src.core.config import get_config
from celery.result import AsyncResult
router = APIRouter()
class BootstrapConfig(BaseModel):
"""Bootstrap training data configuration."""
days: int = 90
timeframe: str = "1h"
min_samples_per_strategy: int = 10
symbols: List[str] = ["BTC/USD", "ETH/USD"]
class MultiSymbolAutopilotConfig(BaseModel):
"""Multi-symbol autopilot configuration."""
symbols: List[str]
mode: str = "intelligent"
auto_execute: bool = False
timeframe: str = "1h"
exchange_id: int = 1
paper_trading: bool = True
interval: float = 60.0
# =============================================================================
# Intelligent Autopilot Endpoints
# =============================================================================
class IntelligentAutopilotConfig(BaseModel):
symbol: str
exchange_id: int = 1
timeframe: str = "1h"
interval: float = 60.0
paper_trading: bool = True
# =============================================================================
# Unified Autopilot Endpoints
# =============================================================================
class UnifiedAutopilotConfig(BaseModel):
"""Unified autopilot configuration (Inteligent Mode)."""
symbol: str
mode: str = "intelligent" # Kept for compatibility but only "intelligent" is supported
auto_execute: bool = False
interval: float = 60.0
exchange_id: int = 1
timeframe: str = "1h"
paper_trading: bool = True
@router.post("/intelligent/start", deprecated=True)
async def start_intelligent_autopilot(
config: IntelligentAutopilotConfig,
background_tasks: BackgroundTasks
):
"""Start the Intelligent Autopilot engine.
.. deprecated:: Use /start-unified instead with mode='intelligent'
"""
try:
autopilot = get_intelligent_autopilot(
symbol=config.symbol,
exchange_id=config.exchange_id,
timeframe=config.timeframe,
interval=config.interval,
paper_trading=config.paper_trading
)
if not autopilot.is_running:
background_tasks.add_task(autopilot.start)
return {
"status": "started",
"symbol": config.symbol,
"timeframe": config.timeframe
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/intelligent/stop")
async def stop_intelligent_autopilot(symbol: str, timeframe: str = "1h"):
"""Stop the Intelligent Autopilot engine."""
try:
autopilot = get_intelligent_autopilot(symbol=symbol, timeframe=timeframe)
autopilot.stop()
return {"status": "stopped", "symbol": symbol}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/intelligent/status/{symbol:path}")
async def get_intelligent_status(symbol: str, timeframe: str = "1h"):
"""Get Intelligent Autopilot status."""
try:
autopilot = get_intelligent_autopilot(symbol=symbol, timeframe=timeframe)
return autopilot.get_status()
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/intelligent/performance")
async def get_intelligent_performance(
strategy_name: Optional[str] = None,
days: int = 30
):
"""Get strategy performance metrics."""
try:
tracker = get_performance_tracker()
if strategy_name:
metrics = tracker.calculate_metrics(strategy_name, period_days=days)
return {"strategy": strategy_name, "metrics": metrics}
else:
# Get all strategies
history = tracker.get_performance_history(days=days)
if history.empty:
return {"strategies": []}
strategies = history['strategy_name'].unique()
results = {}
for strat in strategies:
results[strat] = tracker.calculate_metrics(strat, period_days=days)
return {"strategies": results}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/intelligent/training-stats")
async def get_training_stats(days: int = 365):
"""Get statistics about available training data.
Returns:
Dictionary with total samples and per-strategy counts
"""
try:
tracker = get_performance_tracker()
counts = await tracker.get_strategy_sample_counts(days=days)
return {
"total_samples": sum(counts.values()),
"strategy_counts": counts
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/intelligent/retrain")
async def retrain_model(force: bool = False, bootstrap: bool = True):
"""Manually trigger model retraining (Background Task).
Offloads training to Celery worker.
"""
try:
# Get all bootstrap config to pass to worker
config = get_config()
symbols = config.get("autopilot.intelligent.bootstrap.symbols", ["BTC/USD", "ETH/USD"])
days = config.get("autopilot.intelligent.bootstrap.days", 90)
timeframe = config.get("autopilot.intelligent.bootstrap.timeframe", "1h")
min_samples = config.get("autopilot.intelligent.bootstrap.min_samples_per_strategy", 10)
# Submit to Celery with all configured parameters
task = train_model_task.delay(
force_retrain=force,
bootstrap=bootstrap,
symbols=symbols,
days=days,
timeframe=timeframe,
min_samples_per_strategy=min_samples
)
return {
"status": "queued",
"message": "Model retraining started in background",
"task_id": task.id
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/intelligent/model-info")
async def get_model_info():
"""Get ML model information."""
try:
selector = get_strategy_selector()
return selector.get_model_info()
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/intelligent/reset")
async def reset_model():
"""Reset/delete all saved ML models and training data.
This clears all persisted model files AND training data from database,
allowing for a fresh start with new features.
"""
try:
from pathlib import Path
from src.core.database import get_database, MarketConditionsSnapshot, StrategyPerformance
from sqlalchemy import delete
# Get model directory
model_dir = Path.home() / ".local" / "share" / "crypto_trader" / "models"
deleted_count = 0
if model_dir.exists():
# Delete all strategy selector model files
for model_file in model_dir.glob("strategy_selector_*.joblib"):
model_file.unlink()
deleted_count += 1
# Clear training data from database
db = get_database()
db_cleared = 0
try:
async with db.get_session() as session:
# Delete all market conditions snapshots
result1 = await session.execute(delete(MarketConditionsSnapshot))
# Delete all strategy performance records
result2 = await session.execute(delete(StrategyPerformance))
await session.commit()
db_cleared = result1.rowcount + result2.rowcount
except Exception as e:
# Database clearing is optional - continue even if it fails
pass
# Reset the in-memory model state
selector = get_strategy_selector()
from src.autopilot.models import StrategySelectorModel
selector.model = StrategySelectorModel(model_type="classifier")
return {
"status": "success",
"message": f"Deleted {deleted_count} model file(s) and {db_cleared} training records. Model reset to untrained state.",
"deleted_count": deleted_count,
"db_records_cleared": db_cleared
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# =============================================================================
# Multi-Symbol Autopilot Endpoints
# =============================================================================
@router.post("/multi-symbol/start")
async def start_multi_symbol_autopilot(
config: MultiSymbolAutopilotConfig,
background_tasks: BackgroundTasks
):
"""Start autopilot for multiple symbols simultaneously.
Args:
config: Multi-symbol autopilot configuration
background_tasks: FastAPI background tasks
"""
try:
results = []
for symbol in config.symbols:
# Always use intelligent mode
autopilot = get_intelligent_autopilot(
symbol=symbol,
exchange_id=config.exchange_id,
timeframe=config.timeframe,
interval=config.interval,
paper_trading=config.paper_trading
)
autopilot.enable_auto_execution = config.auto_execute
if not autopilot.is_running:
# Set running flag synchronously before scheduling background task
autopilot._running = True
background_tasks.add_task(autopilot.start)
results.append({"symbol": symbol, "status": "started"})
else:
results.append({"symbol": symbol, "status": "already_running"})
return {
"status": "success",
"mode": "intelligent",
"symbols": results
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/multi-symbol/stop")
async def stop_multi_symbol_autopilot(
symbols: List[str],
mode: str = "intelligent",
timeframe: str = "1h"
):
"""Stop autopilot for multiple symbols.
Args:
symbols: List of symbols to stop
mode: Autopilot mode (pattern or intelligent)
timeframe: Timeframe for intelligent mode
"""
try:
results = []
for symbol in symbols:
# Always use intelligent mode
autopilot = get_intelligent_autopilot(symbol=symbol, timeframe=timeframe)
autopilot.stop()
results.append({"symbol": symbol, "status": "stopped"})
return {
"status": "success",
"symbols": results
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/multi-symbol/status")
async def get_multi_symbol_status(
symbols: str = "", # Comma-separated list
mode: str = "intelligent",
timeframe: str = "1h"
):
"""Get status for multiple symbols.
Args:
symbols: Comma-separated list of symbols (empty = all running)
mode: Autopilot mode
timeframe: Timeframe for intelligent mode
"""
from src.autopilot.intelligent_autopilot import _intelligent_autopilots
try:
results = []
if symbols:
symbol_list = [s.strip() for s in symbols.split(",")]
else:
# Get all running autopilots (intelligent only)
symbol_list = [key.split(":")[0] for key in _intelligent_autopilots.keys()]
for symbol in symbol_list:
try:
autopilot = get_intelligent_autopilot(symbol=symbol, timeframe=timeframe)
status = autopilot.get_status()
status["symbol"] = symbol
status["mode"] = "intelligent"
results.append(status)
except Exception:
results.append({
"symbol": symbol,
"mode": "intelligent",
"running": False,
"error": "Not found"
})
return {
"mode": mode,
"symbols": results,
"total_running": sum(1 for r in results if r.get("running", False))
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# =============================================================================
# Unified Autopilot Endpoints (New)
# =============================================================================
@router.get("/modes")
async def get_autopilot_modes():
"""Get information about available autopilot modes.
Returns mode descriptions, capabilities, tradeoffs, and comparison data.
"""
try:
return get_autopilot_mode_info()
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/start-unified")
async def start_unified_autopilot(
config: UnifiedAutopilotConfig,
background_tasks: BackgroundTasks
):
"""Start autopilot with unified interface (Intelligent Mode only)."""
try:
# Validate mode (for backward compatibility of API clients sending mode)
if config.mode and config.mode != "intelligent":
# We allow it but will treat it as intelligent if possible, or raise error if critical
pass
# Start ML-based autopilot
autopilot = get_intelligent_autopilot(
symbol=config.symbol,
exchange_id=config.exchange_id,
timeframe=config.timeframe,
interval=config.interval,
paper_trading=config.paper_trading
)
# Set auto-execution if enabled
if config.auto_execute:
autopilot.enable_auto_execution = True
if not autopilot.is_running:
# Schedule background task (state management handled by autopilot.start via Redis)
background_tasks.add_task(autopilot.start)
return {
"status": "started",
"mode": "intelligent",
"symbol": config.symbol,
"timeframe": config.timeframe,
"auto_execute": config.auto_execute
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/stop-unified")
async def stop_unified_autopilot(symbol: str, mode: str, timeframe: str = "1h"):
"""Stop autopilot for a symbol."""
try:
autopilot = get_intelligent_autopilot(symbol=symbol, timeframe=timeframe)
autopilot.stop()
return {"status": "stopped", "symbol": symbol, "mode": "intelligent"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/status-unified/{symbol:path}")
async def get_unified_status(symbol: str, mode: str, timeframe: str = "1h"):
"""Get autopilot status for a symbol."""
try:
autopilot = get_intelligent_autopilot(symbol=symbol, timeframe=timeframe)
# Use distributed status check (Redis)
status = await autopilot.get_distributed_status()
status["mode"] = "intelligent"
return status
except Exception as e:
logger.error(f"Error getting unified status: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/bootstrap-config", response_model=BootstrapConfig)
async def get_bootstrap_config():
"""Get bootstrap training data configuration."""
from src.core.config import get_config
config = get_config()
return BootstrapConfig(
days=config.get("autopilot.intelligent.bootstrap.days", 90),
timeframe=config.get("autopilot.intelligent.bootstrap.timeframe", "1h"),
min_samples_per_strategy=config.get("autopilot.intelligent.bootstrap.min_samples_per_strategy", 10),
symbols=config.get("autopilot.intelligent.bootstrap.symbols", ["BTC/USD", "ETH/USD"]),
)
@router.put("/bootstrap-config")
async def update_bootstrap_config(settings: BootstrapConfig):
"""Update bootstrap training data configuration."""
from src.core.config import get_config
config = get_config()
try:
config.set("autopilot.intelligent.bootstrap.days", settings.days)
config.set("autopilot.intelligent.bootstrap.timeframe", settings.timeframe)
config.set("autopilot.intelligent.bootstrap.min_samples_per_strategy", settings.min_samples_per_strategy)
config.set("autopilot.intelligent.bootstrap.symbols", settings.symbols)
# Also update the strategy selector instance if it exists
selector = get_strategy_selector()
selector.bootstrap_days = settings.days
selector.bootstrap_timeframe = settings.timeframe
selector.min_samples_per_strategy = settings.min_samples_per_strategy
selector.bootstrap_symbols = settings.symbols
return {"status": "success", "message": "Bootstrap configuration updated"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/tasks/{task_id}")
async def get_task_status(task_id: str):
"""Get status of a background task."""
try:
task_result = AsyncResult(task_id)
try:
# Accessing status or result might raise an exception if deserialization fails
status = task_result.status
result_data = task_result.result if task_result.ready() else None
meta_data = task_result.info if status == 'PROGRESS' else None
# serialized exception handling
if isinstance(result_data, Exception):
result_data = {
"error": str(result_data),
"type": type(result_data).__name__,
"detail": str(result_data)
}
elif status == "FAILURE" and (not result_data or result_data == {}):
# If failure but empty result, try to get traceback or use a default message
tb = getattr(task_result, 'traceback', None)
if tb:
result_data = {"error": "Task failed", "detail": str(tb)}
else:
result_data = {"error": "Task failed with no error info", "detail": "Check worker logs for details"}
result = {
"task_id": task_id,
"status": status,
"result": result_data
}
if meta_data:
result["meta"] = meta_data
return result
except Exception as inner_e:
# If Celery fails to get status/result (e.g. serialization error), return FAILURE
# This prevents 500 errors in the API when the task itself failed badly
return {
"task_id": task_id,
"status": "FAILURE",
"result": {"error": str(inner_e), "detail": "Failed to retrieve task status"},
"meta": {"error": "Task retrieval failed"}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))