from typing import Dict, Any, Optional from fastapi import APIRouter, HTTPException from pydantic import BaseModel from sqlalchemy import select from src.core.database import Exchange, get_database from src.core.config import get_config from src.security.key_manager import get_key_manager from src.trading.paper_trading import get_paper_trading router = APIRouter() class RiskSettings(BaseModel): max_drawdown_percent: float daily_loss_limit_percent: float position_size_percent: float class PaperTradingSettings(BaseModel): initial_capital: float fee_exchange: str = "coinbase" # Which exchange's fee model to use class LoggingSettings(BaseModel): level: str dir: str retention_days: int class GeneralSettings(BaseModel): timezone: str = "UTC" theme: str = "dark" currency: str = "USD" class ExchangeCreate(BaseModel): name: str api_key: Optional[str] = None api_secret: Optional[str] = None sandbox: bool = False read_only: bool = True enabled: bool = True class ExchangeUpdate(BaseModel): api_key: Optional[str] = None api_secret: Optional[str] = None sandbox: Optional[bool] = None read_only: Optional[bool] = None enabled: Optional[bool] = None @router.get("/risk") async def get_risk_settings(): """Get risk management settings.""" try: config = get_config() return { "max_drawdown_percent": config.get("risk.max_drawdown_percent", 20.0), "daily_loss_limit_percent": config.get("risk.daily_loss_limit_percent", 5.0), "position_size_percent": config.get("risk.position_size_percent", 2.0), } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.put("/risk") async def update_risk_settings(settings: RiskSettings): """Update risk management settings.""" try: config = get_config() config.set("risk.max_drawdown_percent", settings.max_drawdown_percent) config.set("risk.daily_loss_limit_percent", settings.daily_loss_limit_percent) config.set("risk.position_size_percent", settings.position_size_percent) config.save() return {"status": "success"} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.get("/paper-trading") async def get_paper_trading_settings(): """Get paper trading settings.""" try: config = get_config() fee_exchange = config.get("paper_trading.fee_exchange", "coinbase") # Get fee rates for current exchange fee_rates = config.get(f"trading.exchanges.{fee_exchange}.fees", config.get("trading.default_fees", {"maker": 0.001, "taker": 0.001})) return { "initial_capital": config.get("paper_trading.default_capital", 100.0), "fee_exchange": fee_exchange, "fee_rates": fee_rates, } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.put("/paper-trading") async def update_paper_trading_settings(settings: PaperTradingSettings): """Update paper trading settings.""" try: config = get_config() config.set("paper_trading.default_capital", settings.initial_capital) config.set("paper_trading.fee_exchange", settings.fee_exchange) config.save() return {"status": "success"} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.get("/paper-trading/fee-exchanges") async def get_available_fee_exchanges(): """Get available exchange fee models for paper trading.""" try: config = get_config() exchanges_config = config.get("trading.exchanges", {}) default_fees = config.get("trading.default_fees", {"maker": 0.001, "taker": 0.001}) current = config.get("paper_trading.fee_exchange", "coinbase") exchanges = [{"name": "default", "fees": default_fees}] for name, data in exchanges_config.items(): if "fees" in data: exchanges.append({ "name": name, "fees": data["fees"] }) return { "exchanges": exchanges, "current": current, } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.post("/paper-trading/reset") async def reset_paper_account(): """Reset paper trading account.""" try: paper_trading = get_paper_trading() # Reset to capital from config success = await paper_trading.reset() if success: return {"status": "success", "message": "Paper account reset successfully"} else: raise HTTPException(status_code=500, detail="Failed to reset paper account") except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.get("/logging") async def get_logging_settings(): """Get logging settings.""" try: config = get_config() return { "level": config.get("logging.level", "INFO"), "dir": config.get("logging.dir", ""), "retention_days": config.get("logging.retention_days", 30), } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.put("/logging") async def update_logging_settings(settings: LoggingSettings): """Update logging settings.""" try: config = get_config() config.set("logging.level", settings.level) config.set("logging.dir", settings.dir) config.set("logging.retention_days", settings.retention_days) config.save() return {"status": "success"} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.get("/general") async def get_general_settings(): """Get general settings.""" try: config = get_config() return { "timezone": config.get("general.timezone", "UTC"), "theme": config.get("general.theme", "dark"), "currency": config.get("general.currency", "USD"), } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.put("/general") async def update_general_settings(settings: GeneralSettings): """Update general settings.""" try: config = get_config() config.set("general.timezone", settings.timezone) config.set("general.theme", settings.theme) config.set("general.currency", settings.currency) config.save() return {"status": "success"} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.post("/exchanges") async def create_exchange(exchange: ExchangeCreate): """Create a new exchange.""" try: db = get_database() async with db.get_session() as session: from src.exchanges.factory import ExchangeFactory from src.exchanges.public_data import PublicDataAdapter # Check if this is a public data exchange adapter_class = None try: if hasattr(ExchangeFactory, '_adapters'): adapter_class = ExchangeFactory._adapters.get(exchange.name.lower()) except: pass is_public_data = adapter_class == PublicDataAdapter if adapter_class else False # Only require API keys for non-public-data exchanges if not is_public_data: if not exchange.api_key or not exchange.api_secret: raise HTTPException(status_code=400, detail="API key and secret are required") new_exchange = Exchange( name=exchange.name, enabled=exchange.enabled ) session.add(new_exchange) await session.flush() # Save credentials key_manager = get_key_manager() key_manager.update_exchange( new_exchange.id, api_key=exchange.api_key or "", api_secret=exchange.api_secret or "", read_only=exchange.read_only if not is_public_data else True, sandbox=exchange.sandbox if not is_public_data else False ) await session.commit() return {"id": new_exchange.id, "name": new_exchange.name, "status": "created"} except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.put("/exchanges/{exchange_id}") async def update_exchange(exchange_id: int, exchange: ExchangeUpdate): """Update an existing exchange.""" try: db = get_database() async with db.get_session() as session: stmt = select(Exchange).where(Exchange.id == exchange_id) result = await session.execute(stmt) exchange_obj = result.scalar_one_or_none() if not exchange_obj: raise HTTPException(status_code=404, detail="Exchange not found") key_manager = get_key_manager() credentials = key_manager.get_exchange_credentials(exchange_id) # Update credentials if provided if exchange.api_key is not None or exchange.api_secret is not None: key_manager.update_exchange( exchange_id, api_key=exchange.api_key or credentials.get('api_key', ''), api_secret=exchange.api_secret or credentials.get('api_secret', ''), read_only=exchange.read_only if exchange.read_only is not None else credentials.get('read_only', True), sandbox=exchange.sandbox if exchange.sandbox is not None else credentials.get('sandbox', False) ) if exchange.enabled is not None: exchange_obj.enabled = exchange.enabled await session.commit() return {"status": "success"} except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.delete("/exchanges/{exchange_id}") async def delete_exchange(exchange_id: int): """Delete an exchange.""" try: db = get_database() async with db.get_session() as session: stmt = select(Exchange).where(Exchange.id == exchange_id) result = await session.execute(stmt) exchange = result.scalar_one_or_none() if not exchange: raise HTTPException(status_code=404, detail="Exchange not found") await session.delete(exchange) await session.commit() return {"status": "success"} except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.post("/exchanges/{exchange_id}/test") async def test_exchange_connection(exchange_id: int): """Test exchange connection.""" try: db = get_database() async with db.get_session() as session: stmt = select(Exchange).where(Exchange.id == exchange_id) result = await session.execute(stmt) exchange = result.scalar_one_or_none() if not exchange: raise HTTPException(status_code=404, detail="Exchange not found") from src.exchanges.factory import ExchangeFactory from src.exchanges.public_data import PublicDataAdapter # Get adapter class safely adapter_class = None try: if hasattr(ExchangeFactory, '_adapters'): adapter_class = ExchangeFactory._adapters.get(exchange.name.lower()) except: pass is_public_data = adapter_class == PublicDataAdapter if adapter_class else False key_manager = get_key_manager() if not is_public_data and not key_manager.get_exchange_credentials(exchange_id): raise HTTPException(status_code=400, detail="No credentials found for this exchange") adapter = ExchangeFactory.create(exchange_id) if adapter and adapter.connect(): try: if is_public_data: adapter.get_ticker("BTC/USDT") else: adapter.get_balance() return {"status": "success", "message": "Connection successful"} except Exception as e: return {"status": "error", "message": f"Connected but failed to fetch data: {str(e)}"} else: return {"status": "error", "message": "Failed to connect"} except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=str(e))