- Point Ollama to local instance via host.docker.internal, use gemma3 model - Remove Docker Ollama service (using host's Ollama instead) - Add company-name-to-ticker mapping (Apple→AAPL, Tesla→TSLA, etc.) for RSS articles - Lower signal thresholds for faster feedback with paper trading: - FinBERT confidence: 0.6→0.4, signal strength: 0.3→0.15 - News strategy: article_count 2→1, confidence 0.5→0.3, score ±0.3→±0.15 - Fix market data BarSet access bug (BarSet.__contains__ returns False incorrectly) - Fix market data SIP feed error by switching to IEX feed for free Alpaca accounts - Fix nginx proxy routing for /api/auth/* to api-gateway /auth/* - Add seed_sample_data script - Update tests for new thresholds and alpaca mock modules
169 lines
5.7 KiB
Python
169 lines
5.7 KiB
Python
"""Strategy endpoints — list strategies, weight history, metrics."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from uuid import UUID
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
|
|
|
from services.api_gateway.auth.middleware import get_current_user
|
|
from sqlalchemy import select, desc, func
|
|
|
|
router = APIRouter(prefix="/api/strategies", tags=["strategies"])
|
|
|
|
|
|
@router.get("")
|
|
async def list_strategies(
|
|
request: Request,
|
|
_user: dict = Depends(get_current_user),
|
|
) -> list[dict]:
|
|
"""All strategies with current weights and computed performance fields."""
|
|
from shared.models.trading import Strategy, Trade, TradeStatus
|
|
|
|
db = request.app.state.db_session_factory
|
|
async with db() as session:
|
|
result = await session.execute(select(Strategy))
|
|
strategies = result.scalars().all()
|
|
|
|
# Compute per-strategy stats from trades table
|
|
strategy_stats: dict[UUID, dict] = {}
|
|
for s in strategies:
|
|
trades_result = await session.execute(
|
|
select(Trade).where(
|
|
Trade.strategy_id == s.id,
|
|
Trade.status == TradeStatus.FILLED,
|
|
)
|
|
)
|
|
trades = trades_result.scalars().all()
|
|
total_trades = len(trades)
|
|
winning = sum(1 for t in trades if t.pnl is not None and t.pnl > 0)
|
|
total_pnl = sum(t.pnl for t in trades if t.pnl is not None)
|
|
win_rate = winning / total_trades if total_trades > 0 else 0.0
|
|
strategy_stats[s.id] = {
|
|
"win_rate": round(win_rate, 4),
|
|
"total_pnl": round(total_pnl, 2),
|
|
"total_trades": total_trades,
|
|
}
|
|
|
|
return [
|
|
{
|
|
"id": str(s.id),
|
|
"name": s.name,
|
|
"description": s.description,
|
|
"current_weight": s.current_weight,
|
|
"active": s.active,
|
|
"win_rate": strategy_stats[s.id]["win_rate"],
|
|
"total_pnl": strategy_stats[s.id]["total_pnl"],
|
|
"total_trades": strategy_stats[s.id]["total_trades"],
|
|
"created_at": s.created_at.isoformat() if s.created_at else None,
|
|
}
|
|
for s in strategies
|
|
]
|
|
|
|
|
|
@router.get("/weight-history")
|
|
async def get_all_weight_history(
|
|
request: Request,
|
|
_user: dict = Depends(get_current_user),
|
|
) -> list[dict]:
|
|
"""Aggregated weight history pivoted by timestamp for chart display.
|
|
|
|
Returns data in the format:
|
|
``[{"timestamp": "...", "momentum": 0.35, "mean_reversion": 0.30, ...}, ...]``
|
|
"""
|
|
from shared.models.trading import StrategyWeightHistory, Strategy
|
|
|
|
db = request.app.state.db_session_factory
|
|
async with db() as session:
|
|
result = await session.execute(
|
|
select(StrategyWeightHistory, Strategy.name)
|
|
.join(Strategy, StrategyWeightHistory.strategy_id == Strategy.id)
|
|
.order_by(StrategyWeightHistory.created_at)
|
|
.limit(200)
|
|
)
|
|
rows = result.all()
|
|
|
|
# Pivot: group by timestamp, create one object per timestamp
|
|
# with strategy names as keys and new_weight as values
|
|
from collections import OrderedDict
|
|
pivoted: OrderedDict[str, dict] = OrderedDict()
|
|
for h, name in rows:
|
|
ts = h.created_at.isoformat() if h.created_at else ""
|
|
if ts not in pivoted:
|
|
pivoted[ts] = {"timestamp": ts}
|
|
pivoted[ts][name] = h.new_weight
|
|
|
|
return list(pivoted.values())
|
|
|
|
|
|
@router.get("/{strategy_id}/history")
|
|
async def get_strategy_weight_history(
|
|
strategy_id: UUID,
|
|
request: Request,
|
|
_user: dict = Depends(get_current_user),
|
|
) -> list[dict]:
|
|
"""Weight history for a specific strategy."""
|
|
from shared.models.trading import StrategyWeightHistory, Strategy
|
|
|
|
db = request.app.state.db_session_factory
|
|
async with db() as session:
|
|
# Verify strategy exists
|
|
strategy = (
|
|
await session.execute(
|
|
select(Strategy).where(Strategy.id == strategy_id)
|
|
)
|
|
).scalar_one_or_none()
|
|
if strategy is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Strategy not found",
|
|
)
|
|
|
|
result = await session.execute(
|
|
select(StrategyWeightHistory)
|
|
.where(StrategyWeightHistory.strategy_id == strategy_id)
|
|
.order_by(desc(StrategyWeightHistory.created_at))
|
|
)
|
|
history = result.scalars().all()
|
|
|
|
return [
|
|
{
|
|
"id": str(h.id),
|
|
"old_weight": h.old_weight,
|
|
"new_weight": h.new_weight,
|
|
"reason": h.reason,
|
|
"created_at": h.created_at.isoformat() if h.created_at else None,
|
|
}
|
|
for h in history
|
|
]
|
|
|
|
|
|
@router.get("/{strategy_id}/metrics")
|
|
async def get_strategy_metrics(
|
|
strategy_id: UUID,
|
|
request: Request,
|
|
_user: dict = Depends(get_current_user),
|
|
) -> list[dict]:
|
|
"""Performance metrics over time for a specific strategy."""
|
|
from shared.models.timeseries import StrategyMetric
|
|
|
|
db = request.app.state.db_session_factory
|
|
async with db() as session:
|
|
result = await session.execute(
|
|
select(StrategyMetric)
|
|
.where(StrategyMetric.strategy_id == strategy_id)
|
|
.order_by(desc(StrategyMetric.timestamp))
|
|
.limit(100)
|
|
)
|
|
metrics = result.scalars().all()
|
|
|
|
return [
|
|
{
|
|
"timestamp": m.timestamp.isoformat(),
|
|
"win_rate": m.win_rate,
|
|
"total_pnl": m.total_pnl,
|
|
"trade_count": m.trade_count,
|
|
"sharpe_ratio": m.sharpe_ratio,
|
|
}
|
|
for m in metrics
|
|
]
|