Big refactoring
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
All endpoints require admin role.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.dependencies import get_db, require_admin
|
||||
@@ -12,13 +12,16 @@ from app.schemas.admin import (
|
||||
CreateUserRequest,
|
||||
DataCleanupRequest,
|
||||
JobToggle,
|
||||
RecommendationConfigUpdate,
|
||||
PasswordReset,
|
||||
RegistrationToggle,
|
||||
SystemSettingUpdate,
|
||||
TickerUniverseUpdate,
|
||||
UserManagement,
|
||||
)
|
||||
from app.schemas.common import APIEnvelope
|
||||
from app.services import admin_service
|
||||
from app.services import ticker_universe_service
|
||||
|
||||
router = APIRouter(tags=["admin"])
|
||||
|
||||
@@ -123,6 +126,47 @@ async def list_settings(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/admin/settings/recommendations", response_model=APIEnvelope)
|
||||
async def get_recommendation_settings(
|
||||
_admin: User = Depends(require_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
config = await admin_service.get_recommendation_config(db)
|
||||
return APIEnvelope(status="success", data=config)
|
||||
|
||||
|
||||
@router.put("/admin/settings/recommendations", response_model=APIEnvelope)
|
||||
async def update_recommendation_settings(
|
||||
body: RecommendationConfigUpdate,
|
||||
_admin: User = Depends(require_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
updated = await admin_service.update_recommendation_config(
|
||||
db,
|
||||
body.model_dump(exclude_unset=True),
|
||||
)
|
||||
return APIEnvelope(status="success", data=updated)
|
||||
|
||||
|
||||
@router.get("/admin/settings/ticker-universe", response_model=APIEnvelope)
|
||||
async def get_ticker_universe_setting(
|
||||
_admin: User = Depends(require_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
data = await admin_service.get_ticker_universe_default(db)
|
||||
return APIEnvelope(status="success", data=data)
|
||||
|
||||
|
||||
@router.put("/admin/settings/ticker-universe", response_model=APIEnvelope)
|
||||
async def update_ticker_universe_setting(
|
||||
body: TickerUniverseUpdate,
|
||||
_admin: User = Depends(require_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
data = await admin_service.update_ticker_universe_default(db, body.universe)
|
||||
return APIEnvelope(status="success", data=data)
|
||||
|
||||
|
||||
@router.put("/admin/settings/{key}", response_model=APIEnvelope)
|
||||
async def update_setting(
|
||||
key: str,
|
||||
@@ -138,6 +182,21 @@ async def update_setting(
|
||||
)
|
||||
|
||||
|
||||
@router.post("/admin/tickers/bootstrap", response_model=APIEnvelope)
|
||||
async def bootstrap_tickers(
|
||||
universe: str = Query("sp500", pattern="^(sp500|nasdaq100|nasdaq_all)$"),
|
||||
prune_missing: bool = Query(False),
|
||||
_admin: User = Depends(require_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
result = await ticker_universe_service.bootstrap_universe(
|
||||
db,
|
||||
universe,
|
||||
prune_missing=prune_missing,
|
||||
)
|
||||
return APIEnvelope(status="success", data=result)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Data cleanup
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -167,6 +226,15 @@ async def list_jobs(
|
||||
return APIEnvelope(status="success", data=jobs)
|
||||
|
||||
|
||||
@router.get("/admin/pipeline/readiness", response_model=APIEnvelope)
|
||||
async def get_pipeline_readiness(
|
||||
_admin: User = Depends(require_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
data = await admin_service.get_pipeline_readiness(db)
|
||||
return APIEnvelope(status="success", data=data)
|
||||
|
||||
|
||||
@router.post("/admin/jobs/{job_name}/trigger", response_model=APIEnvelope)
|
||||
async def trigger_job(
|
||||
job_name: str,
|
||||
|
||||
@@ -18,10 +18,17 @@ from app.dependencies import get_db, require_access
|
||||
from app.exceptions import ProviderError
|
||||
from app.models.user import User
|
||||
from app.providers.alpaca import AlpacaOHLCVProvider
|
||||
from app.providers.fmp import FMPFundamentalProvider
|
||||
from app.providers.fundamentals_chain import build_fundamental_provider_chain
|
||||
from app.providers.openai_sentiment import OpenAISentimentProvider
|
||||
from app.services.rr_scanner_service import scan_ticker
|
||||
from app.schemas.common import APIEnvelope
|
||||
from app.services import fundamental_service, ingestion_service, sentiment_service
|
||||
from app.services import (
|
||||
fundamental_service,
|
||||
ingestion_service,
|
||||
scoring_service,
|
||||
sentiment_service,
|
||||
sr_service,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -99,10 +106,10 @@ async def fetch_symbol(
|
||||
}
|
||||
|
||||
# --- Fundamentals ---
|
||||
if settings.fmp_api_key:
|
||||
if settings.fmp_api_key or settings.finnhub_api_key or settings.alpha_vantage_api_key:
|
||||
try:
|
||||
fmp_provider = FMPFundamentalProvider(settings.fmp_api_key)
|
||||
fdata = await fmp_provider.fetch_fundamentals(symbol_upper)
|
||||
fundamentals_provider = build_fundamental_provider_chain()
|
||||
fdata = await fundamentals_provider.fetch_fundamentals(symbol_upper)
|
||||
await fundamental_service.store_fundamental(
|
||||
db,
|
||||
symbol=symbol_upper,
|
||||
@@ -119,9 +126,50 @@ async def fetch_symbol(
|
||||
else:
|
||||
sources["fundamentals"] = {
|
||||
"status": "skipped",
|
||||
"message": "FMP API key not configured",
|
||||
"message": "No fundamentals provider key configured",
|
||||
}
|
||||
|
||||
# --- Derived pipeline: S/R levels ---
|
||||
try:
|
||||
levels = await sr_service.recalculate_sr_levels(db, symbol_upper)
|
||||
sources["sr_levels"] = {
|
||||
"status": "ok",
|
||||
"count": len(levels),
|
||||
"message": None,
|
||||
}
|
||||
except Exception as exc:
|
||||
logger.error("S/R recalc failed for %s: %s", symbol_upper, exc)
|
||||
sources["sr_levels"] = {"status": "error", "message": str(exc)}
|
||||
|
||||
# --- Derived pipeline: scores ---
|
||||
try:
|
||||
score_payload = await scoring_service.get_score(db, symbol_upper)
|
||||
sources["scores"] = {
|
||||
"status": "ok",
|
||||
"composite_score": score_payload.get("composite_score"),
|
||||
"missing_dimensions": score_payload.get("missing_dimensions", []),
|
||||
"message": None,
|
||||
}
|
||||
except Exception as exc:
|
||||
logger.error("Score recompute failed for %s: %s", symbol_upper, exc)
|
||||
sources["scores"] = {"status": "error", "message": str(exc)}
|
||||
|
||||
# --- Derived pipeline: scanner ---
|
||||
try:
|
||||
setups = await scan_ticker(
|
||||
db,
|
||||
symbol_upper,
|
||||
rr_threshold=settings.default_rr_threshold,
|
||||
)
|
||||
sources["scanner"] = {
|
||||
"status": "ok",
|
||||
"setups_found": len(setups),
|
||||
"message": None,
|
||||
}
|
||||
except Exception as exc:
|
||||
logger.error("Scanner run failed for %s: %s", symbol_upper, exc)
|
||||
sources["scanner"] = {"status": "error", "message": str(exc)}
|
||||
|
||||
# Always return success — per-source breakdown tells the full story
|
||||
return APIEnvelope(
|
||||
status="success",
|
||||
|
||||
@@ -5,8 +5,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.dependencies import get_db, require_access
|
||||
from app.schemas.common import APIEnvelope
|
||||
from app.schemas.trade_setup import TradeSetupResponse
|
||||
from app.services.rr_scanner_service import get_trade_setups
|
||||
from app.schemas.trade_setup import RecommendationSummaryResponse, TradeSetupResponse
|
||||
from app.services.rr_scanner_service import get_trade_setup_history, get_trade_setups
|
||||
|
||||
router = APIRouter(tags=["trades"])
|
||||
|
||||
@@ -16,13 +16,73 @@ async def list_trade_setups(
|
||||
direction: str | None = Query(
|
||||
None, description="Filter by direction: long or short"
|
||||
),
|
||||
min_confidence: float | None = Query(
|
||||
None, ge=0, le=100, description="Minimum confidence score"
|
||||
),
|
||||
recommended_action: str | None = Query(
|
||||
None,
|
||||
description="Filter by action: LONG_HIGH, LONG_MODERATE, SHORT_HIGH, SHORT_MODERATE, NEUTRAL",
|
||||
),
|
||||
_user=Depends(require_access),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> APIEnvelope:
|
||||
"""Get all trade setups sorted by R:R desc, secondary composite desc.
|
||||
"""Get latest trade setups with recommendation data."""
|
||||
rows = await get_trade_setups(
|
||||
db,
|
||||
direction=direction,
|
||||
min_confidence=min_confidence,
|
||||
recommended_action=recommended_action,
|
||||
)
|
||||
|
||||
data = []
|
||||
for row in rows:
|
||||
summary = RecommendationSummaryResponse(
|
||||
action=row.get("recommended_action") or "NEUTRAL",
|
||||
reasoning=row.get("reasoning"),
|
||||
risk_level=row.get("risk_level"),
|
||||
composite_score=row["composite_score"],
|
||||
)
|
||||
payload = {**row, "recommendation_summary": summary}
|
||||
data.append(TradeSetupResponse(**payload).model_dump(mode="json"))
|
||||
|
||||
Optional direction filter (long/short).
|
||||
"""
|
||||
rows = await get_trade_setups(db, direction=direction)
|
||||
data = [TradeSetupResponse(**r).model_dump(mode="json") for r in rows]
|
||||
return APIEnvelope(status="success", data=data)
|
||||
|
||||
|
||||
@router.get("/trades/{symbol}", response_model=APIEnvelope)
|
||||
async def get_ticker_trade_setups(
|
||||
symbol: str,
|
||||
_user=Depends(require_access),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> APIEnvelope:
|
||||
rows = await get_trade_setups(db, symbol=symbol)
|
||||
data = []
|
||||
for row in rows:
|
||||
summary = RecommendationSummaryResponse(
|
||||
action=row.get("recommended_action") or "NEUTRAL",
|
||||
reasoning=row.get("reasoning"),
|
||||
risk_level=row.get("risk_level"),
|
||||
composite_score=row["composite_score"],
|
||||
)
|
||||
payload = {**row, "recommendation_summary": summary}
|
||||
data.append(TradeSetupResponse(**payload).model_dump(mode="json"))
|
||||
return APIEnvelope(status="success", data=data)
|
||||
|
||||
|
||||
@router.get("/trades/{symbol}/history", response_model=APIEnvelope)
|
||||
async def get_ticker_trade_history(
|
||||
symbol: str,
|
||||
_user=Depends(require_access),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> APIEnvelope:
|
||||
rows = await get_trade_setup_history(db, symbol=symbol)
|
||||
data = []
|
||||
for row in rows:
|
||||
summary = RecommendationSummaryResponse(
|
||||
action=row.get("recommended_action") or "NEUTRAL",
|
||||
reasoning=row.get("reasoning"),
|
||||
risk_level=row.get("risk_level"),
|
||||
composite_score=row["composite_score"],
|
||||
)
|
||||
payload = {**row, "recommendation_summary": summary}
|
||||
data.append(TradeSetupResponse(**payload).model_dump(mode="json"))
|
||||
return APIEnvelope(status="success", data=data)
|
||||
|
||||
Reference in New Issue
Block a user