Big refactoring
Some checks failed
Deploy / lint (push) Failing after 21s
Deploy / test (push) Has been skipped
Deploy / deploy (push) Has been skipped

This commit is contained in:
Dennis Thiessen
2026-03-03 15:20:18 +01:00
parent 181cfe6588
commit 0a011d4ce9
55 changed files with 6898 additions and 544 deletions

View File

@@ -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,

View File

@@ -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",

View File

@@ -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)