Migrate all agent roles from Legacy to EvoAgent architecture: - fundamentals_analyst, technical_analyst, sentiment_analyst, valuation_analyst - risk_manager, portfolio_manager Key changes: - EvoAgent now supports Portfolio Manager compatibility methods (_make_decision, get_decisions, get_portfolio_state, load_portfolio_state, update_portfolio) - Add UnifiedAgentFactory for centralized agent creation - ToolGuard with batch approval API and WebSocket broadcast - Legacy agents marked deprecated (AnalystAgent, RiskAgent, PMAgent) - Remove backend/agents/compat.py migration shim - Add run_id alongside workspace_id for semantic clarity - Complete integration test coverage (13 tests) - All smoke tests passing for 6 agent roles Constraint: Must maintain backward compatibility with existing run configs Constraint: Memory support must work with EvoAgent (no fallback to Legacy) Rejected: Separate PM implementation for EvoAgent | unified approach cleaner Confidence: high Scope-risk: broad Directive: EVO_AGENT_IDS env var still respected but defaults to all roles Not-tested: Kubernetes sandbox mode for skill execution
405 lines
13 KiB
Python
405 lines
13 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Tests for the extracted trading service app surface."""
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
from backend.apps.trading_service import create_app
|
|
from shared.schema import CompanyNews, FinancialMetrics, InsiderTrade, LineItem, Price
|
|
|
|
|
|
def test_trading_service_routes_are_exposed():
|
|
app = create_app()
|
|
|
|
paths = {route.path for route in app.routes}
|
|
|
|
assert "/health" in paths
|
|
assert "/api/prices" in paths
|
|
assert "/api/financials" in paths
|
|
assert "/api/news" in paths
|
|
assert "/api/insider-trades" in paths
|
|
assert "/api/market/status" in paths
|
|
assert "/api/market-cap" in paths
|
|
assert "/api/line-items" in paths
|
|
|
|
|
|
def test_trading_service_prices_endpoint(monkeypatch):
|
|
monkeypatch.setattr(
|
|
"backend.domains.trading.get_prices_payload",
|
|
lambda ticker, start_date, end_date: {
|
|
"ticker": ticker,
|
|
"prices": [
|
|
Price(
|
|
open=1.0,
|
|
close=2.0,
|
|
high=2.5,
|
|
low=0.5,
|
|
volume=100,
|
|
time="2026-03-20",
|
|
)
|
|
],
|
|
},
|
|
)
|
|
|
|
with TestClient(create_app()) as client:
|
|
response = client.get(
|
|
"/api/prices",
|
|
params={
|
|
"ticker": "AAPL",
|
|
"start_date": "2026-03-01",
|
|
"end_date": "2026-03-20",
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["ticker"] == "AAPL"
|
|
assert response.json()["prices"][0]["close"] == 2.0
|
|
|
|
|
|
def test_trading_service_financials_endpoint(monkeypatch):
|
|
monkeypatch.setattr(
|
|
"backend.domains.trading.get_financials_payload",
|
|
lambda ticker, end_date, period, limit: {
|
|
"financial_metrics": [
|
|
FinancialMetrics(
|
|
ticker=ticker,
|
|
report_period=end_date,
|
|
period=period,
|
|
currency="USD",
|
|
market_cap=123.0,
|
|
enterprise_value=None,
|
|
price_to_earnings_ratio=None,
|
|
price_to_book_ratio=None,
|
|
price_to_sales_ratio=None,
|
|
enterprise_value_to_ebitda_ratio=None,
|
|
enterprise_value_to_revenue_ratio=None,
|
|
free_cash_flow_yield=None,
|
|
peg_ratio=None,
|
|
gross_margin=None,
|
|
operating_margin=None,
|
|
net_margin=None,
|
|
return_on_equity=None,
|
|
return_on_assets=None,
|
|
return_on_invested_capital=None,
|
|
asset_turnover=None,
|
|
inventory_turnover=None,
|
|
receivables_turnover=None,
|
|
days_sales_outstanding=None,
|
|
operating_cycle=None,
|
|
working_capital_turnover=None,
|
|
current_ratio=None,
|
|
quick_ratio=None,
|
|
cash_ratio=None,
|
|
operating_cash_flow_ratio=None,
|
|
debt_to_equity=None,
|
|
debt_to_assets=None,
|
|
interest_coverage=None,
|
|
revenue_growth=None,
|
|
earnings_growth=None,
|
|
book_value_growth=None,
|
|
earnings_per_share_growth=None,
|
|
free_cash_flow_growth=None,
|
|
operating_income_growth=None,
|
|
ebitda_growth=None,
|
|
payout_ratio=None,
|
|
earnings_per_share=None,
|
|
book_value_per_share=None,
|
|
free_cash_flow_per_share=None,
|
|
)
|
|
]
|
|
},
|
|
)
|
|
|
|
with TestClient(create_app()) as client:
|
|
response = client.get(
|
|
"/api/financials",
|
|
params={"ticker": "AAPL", "end_date": "2026-03-20"},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["financial_metrics"][0]["ticker"] == "AAPL"
|
|
|
|
|
|
def test_trading_service_news_and_insider_endpoints(monkeypatch):
|
|
monkeypatch.setattr(
|
|
"backend.domains.trading.get_news_payload",
|
|
lambda ticker, end_date, start_date=None, limit=1000: {
|
|
"news": [
|
|
CompanyNews(
|
|
ticker=ticker,
|
|
title="News title",
|
|
source="polygon",
|
|
url="https://example.com/news",
|
|
date=end_date,
|
|
)
|
|
]
|
|
},
|
|
)
|
|
monkeypatch.setattr(
|
|
"backend.domains.trading.get_insider_trades_payload",
|
|
lambda ticker, end_date, start_date=None, limit=1000: {
|
|
"insider_trades": [
|
|
InsiderTrade(ticker=ticker, filing_date=end_date)
|
|
]
|
|
},
|
|
)
|
|
|
|
with TestClient(create_app()) as client:
|
|
news_response = client.get(
|
|
"/api/news",
|
|
params={"ticker": "AAPL", "end_date": "2026-03-20"},
|
|
)
|
|
insider_response = client.get(
|
|
"/api/insider-trades",
|
|
params={"ticker": "AAPL", "end_date": "2026-03-20"},
|
|
)
|
|
|
|
assert news_response.status_code == 200
|
|
assert news_response.json()["news"][0]["title"] == "News title"
|
|
assert insider_response.status_code == 200
|
|
assert insider_response.json()["insider_trades"][0]["ticker"] == "AAPL"
|
|
|
|
|
|
def test_trading_service_market_status_endpoint(monkeypatch):
|
|
class _FakeMarketService:
|
|
def get_market_status(self):
|
|
return {"status": "open", "status_text": "Open"}
|
|
|
|
monkeypatch.setattr(
|
|
"backend.domains.trading.get_market_status_payload",
|
|
lambda: _FakeMarketService().get_market_status(),
|
|
)
|
|
|
|
with TestClient(create_app()) as client:
|
|
response = client.get("/api/market/status")
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {"status": "open", "status_text": "Open"}
|
|
|
|
|
|
def test_trading_service_market_cap_endpoint(monkeypatch):
|
|
monkeypatch.setattr(
|
|
"backend.domains.trading.get_market_cap_payload",
|
|
lambda ticker, end_date: {
|
|
"ticker": ticker,
|
|
"end_date": end_date,
|
|
"market_cap": 3.5e12,
|
|
},
|
|
)
|
|
|
|
with TestClient(create_app()) as client:
|
|
response = client.get(
|
|
"/api/market-cap",
|
|
params={"ticker": "AAPL", "end_date": "2026-03-20"},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {
|
|
"ticker": "AAPL",
|
|
"end_date": "2026-03-20",
|
|
"market_cap": 3.5e12,
|
|
}
|
|
|
|
|
|
def test_trading_service_contract_stability():
|
|
"""Verify trading service API maintains contract stability."""
|
|
app = create_app()
|
|
routes = {route.path: route for route in app.routes if hasattr(route, "methods")}
|
|
|
|
# Health endpoint
|
|
assert "/health" in routes
|
|
|
|
# Trading data endpoints
|
|
assert "/api/prices" in routes
|
|
assert "/api/financials" in routes
|
|
assert "/api/news" in routes
|
|
assert "/api/insider-trades" in routes
|
|
assert "/api/market/status" in routes
|
|
assert "/api/market-cap" in routes
|
|
assert "/api/line-items" in routes
|
|
|
|
# Verify all are GET endpoints (read-only service)
|
|
for path in ["/api/prices", "/api/financials", "/api/news", "/api/insider-trades",
|
|
"/api/market/status", "/api/market-cap", "/api/line-items"]:
|
|
assert "GET" in routes[path].methods
|
|
|
|
|
|
def test_trading_service_prices_contract(monkeypatch):
|
|
"""Test prices endpoint maintains response contract."""
|
|
monkeypatch.setattr(
|
|
"backend.domains.trading.get_prices_payload",
|
|
lambda ticker, start_date, end_date: {
|
|
"ticker": ticker,
|
|
"prices": [
|
|
Price(
|
|
open=1.0,
|
|
close=2.0,
|
|
high=2.5,
|
|
low=0.5,
|
|
volume=100,
|
|
time="2026-03-20",
|
|
)
|
|
],
|
|
},
|
|
)
|
|
|
|
with TestClient(create_app()) as client:
|
|
response = client.get(
|
|
"/api/prices",
|
|
params={
|
|
"ticker": "AAPL",
|
|
"start_date": "2026-03-01",
|
|
"end_date": "2026-03-20",
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
assert "ticker" in payload
|
|
assert "prices" in payload
|
|
assert isinstance(payload["prices"], list)
|
|
if payload["prices"]:
|
|
price = payload["prices"][0]
|
|
assert "open" in price
|
|
assert "close" in price
|
|
assert "high" in price
|
|
assert "low" in price
|
|
assert "volume" in price
|
|
assert "time" in price
|
|
|
|
|
|
def test_trading_service_financials_contract(monkeypatch):
|
|
"""Test financials endpoint maintains response contract."""
|
|
monkeypatch.setattr(
|
|
"backend.domains.trading.get_financials_payload",
|
|
lambda ticker, end_date, period, limit: {
|
|
"financial_metrics": [
|
|
FinancialMetrics(
|
|
ticker=ticker,
|
|
report_period=end_date,
|
|
period=period,
|
|
currency="USD",
|
|
market_cap=123.0,
|
|
enterprise_value=None,
|
|
price_to_earnings_ratio=None,
|
|
price_to_book_ratio=None,
|
|
price_to_sales_ratio=None,
|
|
enterprise_value_to_ebitda_ratio=None,
|
|
enterprise_value_to_revenue_ratio=None,
|
|
free_cash_flow_yield=None,
|
|
peg_ratio=None,
|
|
gross_margin=None,
|
|
operating_margin=None,
|
|
net_margin=None,
|
|
return_on_equity=None,
|
|
return_on_assets=None,
|
|
return_on_invested_capital=None,
|
|
asset_turnover=None,
|
|
inventory_turnover=None,
|
|
receivables_turnover=None,
|
|
days_sales_outstanding=None,
|
|
operating_cycle=None,
|
|
working_capital_turnover=None,
|
|
current_ratio=None,
|
|
quick_ratio=None,
|
|
cash_ratio=None,
|
|
operating_cash_flow_ratio=None,
|
|
debt_to_equity=None,
|
|
debt_to_assets=None,
|
|
interest_coverage=None,
|
|
revenue_growth=None,
|
|
earnings_growth=None,
|
|
book_value_growth=None,
|
|
earnings_per_share_growth=None,
|
|
free_cash_flow_growth=None,
|
|
operating_income_growth=None,
|
|
ebitda_growth=None,
|
|
payout_ratio=None,
|
|
earnings_per_share=None,
|
|
book_value_per_share=None,
|
|
free_cash_flow_per_share=None,
|
|
)
|
|
]
|
|
},
|
|
)
|
|
|
|
with TestClient(create_app()) as client:
|
|
response = client.get(
|
|
"/api/financials",
|
|
params={"ticker": "AAPL", "end_date": "2026-03-20"},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
assert "financial_metrics" in payload
|
|
assert isinstance(payload["financial_metrics"], list)
|
|
|
|
|
|
def test_trading_service_market_status_contract(monkeypatch):
|
|
"""Test market status endpoint maintains response contract."""
|
|
monkeypatch.setattr(
|
|
"backend.domains.trading.get_market_status_payload",
|
|
lambda: {"status": "open", "status_text": "Open", "next_open": "09:30"},
|
|
)
|
|
|
|
with TestClient(create_app()) as client:
|
|
response = client.get("/api/market/status")
|
|
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
assert "status" in payload
|
|
|
|
|
|
def test_trading_service_market_cap_contract(monkeypatch):
|
|
"""Test market cap endpoint maintains response contract."""
|
|
monkeypatch.setattr(
|
|
"backend.domains.trading.get_market_cap_payload",
|
|
lambda ticker, end_date: {
|
|
"ticker": ticker,
|
|
"end_date": end_date,
|
|
"market_cap": 3.5e12,
|
|
},
|
|
)
|
|
|
|
with TestClient(create_app()) as client:
|
|
response = client.get(
|
|
"/api/market-cap",
|
|
params={"ticker": "AAPL", "end_date": "2026-03-20"},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
assert "ticker" in payload
|
|
assert "end_date" in payload
|
|
assert "market_cap" in payload
|
|
|
|
|
|
def test_trading_service_line_items_endpoint(monkeypatch):
|
|
monkeypatch.setattr(
|
|
"backend.domains.trading.get_line_items_payload",
|
|
lambda ticker, line_items, end_date, period, limit: {
|
|
"search_results": [
|
|
LineItem(
|
|
ticker=ticker,
|
|
report_period=end_date,
|
|
period=period,
|
|
currency="USD",
|
|
free_cash_flow=123.0,
|
|
)
|
|
]
|
|
},
|
|
)
|
|
|
|
with TestClient(create_app()) as client:
|
|
response = client.get(
|
|
"/api/line-items",
|
|
params=[
|
|
("ticker", "AAPL"),
|
|
("line_items", "free_cash_flow"),
|
|
("end_date", "2026-03-20"),
|
|
],
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["search_results"][0]["ticker"] == "AAPL"
|
|
assert response.json()["search_results"][0]["free_cash_flow"] == 123.0
|