Local changes: Updated model training, removed debug instrumentation, and configuration improvements

This commit is contained in:
kfox
2025-12-26 01:15:43 -05:00
commit cc60da49e7
388 changed files with 57127 additions and 0 deletions

564
backend/api/autopilot.py Normal file
View File

@@ -0,0 +1,564 @@
"""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))