feat(api): /api/meet-kevin/strategy/* routes
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed

This commit is contained in:
Viktor Barzin 2026-05-24 01:12:16 +00:00
parent 886dbaec86
commit 06ede26e78
3 changed files with 396 additions and 0 deletions

View file

@ -0,0 +1,153 @@
"""Tests for /api/meet-kevin/strategy/* + /positions/{symbol}/close routes."""
from __future__ import annotations
from datetime import datetime, timezone
from decimal import Decimal
from unittest.mock import AsyncMock, MagicMock
import pytest
from fastapi.testclient import TestClient
from services.api_gateway.auth.jwt import create_access_token
from services.api_gateway.auth.middleware import get_config, get_current_user
from services.api_gateway.config import ApiGatewayConfig
from services.api_gateway.main import create_app
@pytest.fixture
def config() -> ApiGatewayConfig:
return ApiGatewayConfig(
jwt_secret_key="test-secret-for-routes-kevin-strategy",
database_url="sqlite+aiosqlite:///:memory:",
redis_url="redis://localhost:6379/0",
)
@pytest.fixture
def auth_headers(config: ApiGatewayConfig) -> dict[str, str]:
token = create_access_token("user-test", "tester", config)
return {"Authorization": f"Bearer {token}"}
def _make_session():
session = AsyncMock()
session.__aenter__ = AsyncMock(return_value=session)
session.__aexit__ = AsyncMock(return_value=False)
factory = MagicMock()
factory.return_value = session
return factory, session
def _result(scalars: list, scalar_one_or_none=None):
result = MagicMock()
result.scalars.return_value.all.return_value = scalars
result.scalar_one_or_none.return_value = scalar_one_or_none
return result
@pytest.fixture
def app(config: ApiGatewayConfig):
factory, session = _make_session()
app = create_app(config)
app.dependency_overrides[get_current_user] = lambda: {
"sub": "user-test",
"username": "tester",
"type": "access",
}
app.dependency_overrides[get_config] = lambda: config
app.state.db_session_factory = factory
app.state.config = config
app.state.redis = AsyncMock()
app.state.redis.get = AsyncMock(return_value=None)
app.state.redis.set = AsyncMock()
app.state.db_engine = MagicMock()
app._test_session = session # type: ignore[attr-defined]
return app
def test_tickers_returns_empty_when_no_mentions(app, auth_headers):
session = app._test_session # type: ignore[attr-defined]
session.execute = AsyncMock(return_value=_result([]))
client = TestClient(app, raise_server_exceptions=False)
resp = client.get("/api/meet-kevin/strategy/tickers", headers=auth_headers)
assert resp.status_code == 200
assert resp.json() == []
def test_tickers_joins_mention_audit_trade(app, auth_headers):
mention = MagicMock(
id=1,
symbol="NVDA",
action=MagicMock(value="buy"),
conviction=Decimal("0.7"),
time_horizon=MagicMock(value="weeks"),
created_at=datetime.now(timezone.utc),
)
audit = MagicMock(
mention_id=1,
bridge_status=MagicMock(value="emitted"),
effective_conviction=Decimal("0.75"),
)
session = app._test_session # type: ignore[attr-defined]
session.execute = AsyncMock(
side_effect=[
_result([mention]),
_result([audit]),
_result([]), # no trades
]
)
client = TestClient(app, raise_server_exceptions=False)
resp = client.get("/api/meet-kevin/strategy/tickers", headers=auth_headers)
assert resp.status_code == 200
rows = resp.json()
assert len(rows) == 1
assert rows[0]["symbol"] == "NVDA"
assert rows[0]["bridge_status"] == "emitted"
assert rows[0]["effective_conviction"] == 0.75
assert rows[0]["has_open_trade"] is False
def test_performance_returns_aggregates(app, auth_headers):
closed_win = MagicMock(pnl=100.0)
closed_loss = MagicMock(pnl=-50.0)
session = app._test_session # type: ignore[attr-defined]
session.execute = AsyncMock(
return_value=_result([closed_win, closed_loss])
)
client = TestClient(app, raise_server_exceptions=False)
resp = client.get("/api/meet-kevin/strategy/performance", headers=auth_headers)
assert resp.status_code == 200
data = resp.json()
assert data["closed_trade_count"] == 2
assert data["wins"] == 1
assert data["losses"] == 1
def test_close_position_writes_redis_flag(app, auth_headers):
client = TestClient(app, raise_server_exceptions=False)
resp = client.post(
"/api/meet-kevin/positions/nvda/close", headers=auth_headers
)
assert resp.status_code == 200
assert resp.json()["symbol"] == "NVDA"
redis = app.state.redis
redis.set.assert_awaited()
def test_equity_curve_returns_kevin_series(app, auth_headers):
snap = MagicMock(
timestamp=datetime(2026, 5, 1, tzinfo=timezone.utc),
total_value=100000.0,
)
session = app._test_session # type: ignore[attr-defined]
session.execute = AsyncMock(return_value=_result([snap]))
client = TestClient(app, raise_server_exceptions=False)
resp = client.get(
"/api/meet-kevin/strategy/equity-curve", headers=auth_headers
)
assert resp.status_code == 200
data = resp.json()
assert "kevin_equity_curve" in data
assert data["kevin_equity_curve"][0][1] == 100000.0