galaxis-po/backend/app/api/strategy.py

107 lines
3.2 KiB
Python
Raw Normal View History

"""
Quant strategy API endpoints.
"""
from typing import Set
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.api.deps import CurrentUser
from app.models.stock import ETF
from app.schemas.strategy import (
MultiFactorRequest, QualityRequest, ValueMomentumRequest, KJBRequest, StrategyResult,
)
from app.services.strategy import MultiFactorStrategy, QualityStrategy, ValueMomentumStrategy, KJBStrategy
router = APIRouter(prefix="/api/strategy", tags=["strategy"])
def _filter_dc_only(result: StrategyResult, db: Session) -> StrategyResult:
"""Filter strategy result to include only ETFs (DC pension investable)."""
tickers = [s.ticker for s in result.stocks]
etf_tickers: Set[str] = set(
row[0] for row in db.query(ETF.ticker).filter(ETF.ticker.in_(tickers)).all()
) if tickers else set()
filtered = [s for s in result.stocks if s.ticker in etf_tickers]
# Re-rank
for i, stock in enumerate(filtered, 1):
stock.rank = i
return StrategyResult(
strategy_name=result.strategy_name,
base_date=result.base_date,
universe_count=result.universe_count,
result_count=len(filtered),
stocks=filtered,
)
@router.post("/multi-factor", response_model=StrategyResult)
async def run_multi_factor(
request: MultiFactorRequest,
current_user: CurrentUser,
db: Session = Depends(get_db),
):
"""Run multi-factor strategy."""
strategy = MultiFactorStrategy(db)
result = strategy.run(
universe_filter=request.universe,
top_n=request.top_n,
base_date=request.base_date,
weights=request.weights,
)
return _filter_dc_only(result, db) if request.dc_only else result
@router.post("/quality", response_model=StrategyResult)
async def run_quality(
request: QualityRequest,
current_user: CurrentUser,
db: Session = Depends(get_db),
):
"""Run super quality strategy."""
strategy = QualityStrategy(db)
result = strategy.run(
universe_filter=request.universe,
top_n=request.top_n,
base_date=request.base_date,
min_fscore=request.min_fscore,
)
return _filter_dc_only(result, db) if request.dc_only else result
@router.post("/value-momentum", response_model=StrategyResult)
async def run_value_momentum(
request: ValueMomentumRequest,
current_user: CurrentUser,
db: Session = Depends(get_db),
):
"""Run value-momentum strategy."""
strategy = ValueMomentumStrategy(db)
result = strategy.run(
universe_filter=request.universe,
top_n=request.top_n,
base_date=request.base_date,
value_weight=request.value_weight,
momentum_weight=request.momentum_weight,
)
return _filter_dc_only(result, db) if request.dc_only else result
@router.post("/kjb", response_model=StrategyResult)
async def run_kjb(
request: KJBRequest,
current_user: CurrentUser,
db: Session = Depends(get_db),
):
"""Run KJB strategy."""
strategy = KJBStrategy(db)
result = strategy.run(
universe_filter=request.universe,
top_n=request.top_n,
base_date=request.base_date,
)
return _filter_dc_only(result, db) if request.dc_only else result