feat: add algorithm settings backend + integration guide
- Create /api/v1/settings/algorithms endpoint for algorithm management - Enable/disable Algorithm #15 (liquidity_sweep) and #16 (price_action_reversal) - Settings persist in User.preferences JSON column - Settings wired to signal_scoring filter (disabled algos vote 0.0) - Add comprehensive ALGORITHM_INTEGRATION_GUIDE.md documentation - Add unit tests for both algorithms in isolation and together - Vote weights: liquidity_sweep ±2.0, price_action_reversal ±2.5 - Correlation dampening: 0.45 when both vote same direction (pattern group) - Tested: algorithms called in get_indicators(), passed through signal pipeline
This commit is contained in:
@@ -18,6 +18,7 @@ from app.api.v1.strategies import router as strategies_router
|
||||
from app.api.v1.analytics import router as analytics_router
|
||||
from app.api.v1.audit import router as audit_router
|
||||
from app.api.v1.alerts import router as alerts_router
|
||||
from app.api.v1.settings import router as settings_router
|
||||
|
||||
api_router = APIRouter(prefix="/api/v1")
|
||||
api_router.include_router(auth_router)
|
||||
@@ -36,6 +37,7 @@ api_router.include_router(strategies_router)
|
||||
api_router.include_router(analytics_router)
|
||||
api_router.include_router(audit_router)
|
||||
api_router.include_router(alerts_router)
|
||||
api_router.include_router(settings_router)
|
||||
|
||||
|
||||
# ── Users endpoint (no prefix, directly on api_router) ──
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
"""Settings API — algorithm toggles and configuration management."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_current_user, get_db_session
|
||||
from app.models.user import User
|
||||
from app.schemas.settings import (
|
||||
AlgorithmSettingsResponse,
|
||||
AlgorithmToggleRequest,
|
||||
AlgorithmConfigResponse,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/settings", tags=["settings"])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Algorithm Configuration
|
||||
# ---------------------------------------------------------------------------
|
||||
# Map algorithm ID to name and default enabled state
|
||||
ALGORITHM_CONFIGS = {
|
||||
"double_bb_rsi": {
|
||||
"name": "Double Bollinger Bands + RSI",
|
||||
"description": "Volatility + momentum oscillator",
|
||||
"enabled": True,
|
||||
"weight": 1.0,
|
||||
},
|
||||
"macd_crossover": {
|
||||
"name": "MACD Crossover",
|
||||
"description": "Trend-following momentum indicator",
|
||||
"enabled": True,
|
||||
"weight": 1.0,
|
||||
},
|
||||
"supertrend": {
|
||||
"name": "SuperTrend",
|
||||
"description": "Trend detection with ATR-based stops",
|
||||
"enabled": True,
|
||||
"weight": 1.0,
|
||||
},
|
||||
"volume_breakout": {
|
||||
"name": "Volume Breakout",
|
||||
"description": "Volume-driven support/resistance breaks",
|
||||
"enabled": True,
|
||||
"weight": 1.0,
|
||||
},
|
||||
"ichimoku": {
|
||||
"name": "Ichimoku Cloud",
|
||||
"description": "Multi-line equilibrium trend system",
|
||||
"enabled": True,
|
||||
"weight": 1.0,
|
||||
},
|
||||
"divergence": {
|
||||
"name": "RSI/MACD Divergence",
|
||||
"description": "Price-momentum divergence detection",
|
||||
"enabled": True,
|
||||
"weight": 1.0,
|
||||
},
|
||||
"smc": {
|
||||
"name": "Smart Money Concepts (SMC)",
|
||||
"description": "Market structure + order block detection",
|
||||
"enabled": True,
|
||||
"weight": 1.0,
|
||||
},
|
||||
"obv": {
|
||||
"name": "On-Balance Volume (OBV)",
|
||||
"description": "Volume flow and accumulation/distribution",
|
||||
"enabled": True,
|
||||
"weight": 1.0,
|
||||
},
|
||||
"stoch_rsi": {
|
||||
"name": "Stochastic RSI",
|
||||
"description": "RSI momentum in oversold/overbought zones",
|
||||
"enabled": True,
|
||||
"weight": 1.0,
|
||||
},
|
||||
"mfi": {
|
||||
"name": "Money Flow Index (MFI)",
|
||||
"description": "Volume-weighted momentum indicator",
|
||||
"enabled": True,
|
||||
"weight": 1.0,
|
||||
},
|
||||
"fvg": {
|
||||
"name": "Fair Value Gap (FVG)",
|
||||
"description": "Inefficiency zone detection for reversals",
|
||||
"enabled": True,
|
||||
"weight": 1.0,
|
||||
},
|
||||
"candlestick": {
|
||||
"name": "Candlestick Patterns",
|
||||
"description": "Pin bar, engulfing, and other reversal patterns",
|
||||
"enabled": True,
|
||||
"weight": 1.0,
|
||||
},
|
||||
"funding_oi": {
|
||||
"name": "Funding Rate + Open Interest",
|
||||
"description": "Perpetual futures positioning extremes (contrarian)",
|
||||
"enabled": True,
|
||||
"weight": 1.0,
|
||||
},
|
||||
"liquidity_sweep": {
|
||||
"name": "Liquidity Sweep (Algorithm #15)",
|
||||
"description": "Swing high/low liquidity level breaks",
|
||||
"enabled": True,
|
||||
"weight": 2.0,
|
||||
},
|
||||
"price_action_reversal": {
|
||||
"name": "Price Action Reversal (Algorithm #16)",
|
||||
"description": "Pin bar/engulfing at support/resistance zones",
|
||||
"enabled": True,
|
||||
"weight": 2.5,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@router.get("/algorithms", response_model=list[AlgorithmConfigResponse])
|
||||
async def get_algorithm_settings(
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> list[AlgorithmConfigResponse]:
|
||||
"""Get all available algorithms and their current enabled state.
|
||||
|
||||
Returns a list of all algorithms with their configuration, including
|
||||
whether each is enabled for this user and its vote weight.
|
||||
"""
|
||||
prefs = current_user.preferences or {}
|
||||
enabled_algos = prefs.get("enabled_algorithms", {})
|
||||
|
||||
result = []
|
||||
for algo_id, config in ALGORITHM_CONFIGS.items():
|
||||
is_enabled = enabled_algos.get(algo_id, config.get("enabled", True))
|
||||
|
||||
result.append(
|
||||
AlgorithmConfigResponse(
|
||||
id=algo_id,
|
||||
name=config.get("name", algo_id),
|
||||
description=config.get("description", ""),
|
||||
enabled=is_enabled,
|
||||
weight=config.get("weight", 1.0),
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.put("/algorithms/{algorithm_id}", response_model=AlgorithmSettingsResponse)
|
||||
async def toggle_algorithm(
|
||||
algorithm_id: str,
|
||||
body: AlgorithmToggleRequest,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> AlgorithmSettingsResponse:
|
||||
"""Toggle an algorithm on/off for the current user.
|
||||
|
||||
Path Parameters:
|
||||
- algorithm_id: Algorithm identifier (e.g., "liquidity_sweep", "price_action_reversal")
|
||||
|
||||
Request Body:
|
||||
- enabled: boolean to enable/disable the algorithm
|
||||
|
||||
Returns the updated algorithm configuration and its new state.
|
||||
"""
|
||||
if algorithm_id not in ALGORITHM_CONFIGS:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Algorithm '{algorithm_id}' not found",
|
||||
)
|
||||
|
||||
# Initialize preferences if needed
|
||||
if current_user.preferences is None:
|
||||
current_user.preferences = {}
|
||||
|
||||
if "enabled_algorithms" not in current_user.preferences:
|
||||
current_user.preferences["enabled_algorithms"] = {}
|
||||
|
||||
# Update the algorithm's enabled state
|
||||
current_user.preferences["enabled_algorithms"][algorithm_id] = body.enabled
|
||||
|
||||
# Persist to database
|
||||
await db.flush()
|
||||
await db.refresh(current_user)
|
||||
|
||||
config = ALGORITHM_CONFIGS.get(algorithm_id, {})
|
||||
return AlgorithmSettingsResponse(
|
||||
id=algorithm_id,
|
||||
name=config.get("name", algorithm_id),
|
||||
description=config.get("description", ""),
|
||||
enabled=body.enabled,
|
||||
weight=config.get("weight", 1.0),
|
||||
message=f"Algorithm '{algorithm_id}' {'enabled' if body.enabled else 'disabled'}",
|
||||
)
|
||||
|
||||
|
||||
@router.post("/algorithms/reset", response_model=dict)
|
||||
async def reset_algorithm_settings(
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> dict:
|
||||
"""Reset all algorithm settings to defaults (all enabled).
|
||||
|
||||
This endpoint resets the user's algorithm preferences to the system defaults,
|
||||
which enable all algorithms with their standard weights.
|
||||
"""
|
||||
if current_user.preferences is None:
|
||||
current_user.preferences = {}
|
||||
|
||||
# Clear the enabled_algorithms override
|
||||
current_user.preferences["enabled_algorithms"] = {}
|
||||
|
||||
await db.flush()
|
||||
await db.refresh(current_user)
|
||||
|
||||
return {
|
||||
"message": "Algorithm settings reset to defaults",
|
||||
"status": "success",
|
||||
}
|
||||
Reference in New Issue
Block a user