feat: productionize local service — fix signal pipeline, lower thresholds, add company-name ticker extraction

- 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
This commit is contained in:
Viktor Barzin 2026-02-22 22:17:26 +00:00
parent 67e64fab18
commit d36ae40df1
No known key found for this signature in database
GPG key ID: 0EB088298288D958
18 changed files with 749 additions and 185 deletions

View file

@ -7,7 +7,7 @@ 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
from sqlalchemy import select, desc, func
router = APIRouter(prefix="/api/strategies", tags=["strategies"])
@ -17,25 +17,83 @@ async def list_strategies(
request: Request,
_user: dict = Depends(get_current_user),
) -> list[dict]:
"""All strategies with current weights."""
from shared.models.trading import Strategy
"""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()
return [
{
"id": str(s.id),
"name": s.name,
"description": s.description,
"current_weight": s.current_weight,
"active": s.active,
"created_at": s.created_at.isoformat() if s.created_at else None,
}
for s in strategies
]
# 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")
@ -68,16 +126,16 @@ async def get_strategy_weight_history(
)
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
]
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")
@ -99,13 +157,13 @@ async def get_strategy_metrics(
)
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
]
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
]