summaryrefslogtreecommitdiff
path: root/services
diff options
context:
space:
mode:
Diffstat (limited to 'services')
-rw-r--r--services/api/Dockerfile11
-rw-r--r--services/api/pyproject.toml20
-rw-r--r--services/api/src/trading_api/__init__.py0
-rw-r--r--services/api/src/trading_api/main.py34
-rw-r--r--services/api/src/trading_api/routers/__init__.py0
-rw-r--r--services/api/src/trading_api/routers/orders.py54
-rw-r--r--services/api/src/trading_api/routers/portfolio.py41
-rw-r--r--services/api/src/trading_api/routers/strategies.py28
-rw-r--r--services/api/tests/__init__.py0
-rw-r--r--services/api/tests/test_api.py17
10 files changed, 205 insertions, 0 deletions
diff --git a/services/api/Dockerfile b/services/api/Dockerfile
new file mode 100644
index 0000000..b942075
--- /dev/null
+++ b/services/api/Dockerfile
@@ -0,0 +1,11 @@
+FROM python:3.12-slim
+WORKDIR /app
+COPY shared/ shared/
+RUN pip install --no-cache-dir ./shared
+COPY services/api/ services/api/
+RUN pip install --no-cache-dir ./services/api
+COPY services/strategy-engine/strategies/ /app/strategies/
+COPY services/strategy-engine/ services/strategy-engine/
+RUN pip install --no-cache-dir ./services/strategy-engine
+ENV PYTHONPATH=/app
+CMD ["uvicorn", "trading_api.main:app", "--host", "0.0.0.0", "--port", "8000"]
diff --git a/services/api/pyproject.toml b/services/api/pyproject.toml
new file mode 100644
index 0000000..fd2598d
--- /dev/null
+++ b/services/api/pyproject.toml
@@ -0,0 +1,20 @@
+[project]
+name = "trading-api"
+version = "0.1.0"
+description = "REST API for the trading platform"
+requires-python = ">=3.12"
+dependencies = [
+ "fastapi>=0.110",
+ "uvicorn>=0.27",
+ "trading-shared",
+]
+
+[project.optional-dependencies]
+dev = ["pytest>=8.0", "pytest-asyncio>=0.23", "httpx>=0.27"]
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[tool.hatch.build.targets.wheel]
+packages = ["src/trading_api"]
diff --git a/services/api/src/trading_api/__init__.py b/services/api/src/trading_api/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/services/api/src/trading_api/__init__.py
diff --git a/services/api/src/trading_api/main.py b/services/api/src/trading_api/main.py
new file mode 100644
index 0000000..61cfe36
--- /dev/null
+++ b/services/api/src/trading_api/main.py
@@ -0,0 +1,34 @@
+"""Trading Platform REST API."""
+from contextlib import asynccontextmanager
+
+from fastapi import FastAPI
+
+from shared.config import Settings
+from shared.db import Database
+
+from trading_api.routers import portfolio, orders, strategies
+
+
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+ settings = Settings()
+ app.state.db = Database(settings.database_url)
+ await app.state.db.connect()
+ yield
+ await app.state.db.close()
+
+
+app = FastAPI(
+ title="Trading Platform API",
+ version="0.1.0",
+ lifespan=lifespan,
+)
+
+app.include_router(portfolio.router, prefix="/api/v1/portfolio", tags=["portfolio"])
+app.include_router(orders.router, prefix="/api/v1/orders", tags=["orders"])
+app.include_router(strategies.router, prefix="/api/v1/strategies", tags=["strategies"])
+
+
+@app.get("/health")
+async def health():
+ return {"status": "ok"}
diff --git a/services/api/src/trading_api/routers/__init__.py b/services/api/src/trading_api/routers/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/services/api/src/trading_api/routers/__init__.py
diff --git a/services/api/src/trading_api/routers/orders.py b/services/api/src/trading_api/routers/orders.py
new file mode 100644
index 0000000..989694f
--- /dev/null
+++ b/services/api/src/trading_api/routers/orders.py
@@ -0,0 +1,54 @@
+"""Order endpoints."""
+from fastapi import APIRouter, Request
+from shared.sa_models import OrderRow, SignalRow
+from sqlalchemy import select
+
+router = APIRouter()
+
+
+@router.get("/")
+async def get_orders(request: Request, limit: int = 50):
+ """Get recent orders."""
+ db = request.app.state.db
+ async with db.get_session() as session:
+ stmt = select(OrderRow).order_by(OrderRow.created_at.desc()).limit(limit)
+ result = await session.execute(stmt)
+ rows = result.scalars().all()
+ return [
+ {
+ "id": r.id,
+ "signal_id": r.signal_id,
+ "symbol": r.symbol,
+ "side": r.side,
+ "type": r.type,
+ "price": float(r.price),
+ "quantity": float(r.quantity),
+ "status": r.status,
+ "created_at": r.created_at.isoformat() if r.created_at else None,
+ "filled_at": r.filled_at.isoformat() if r.filled_at else None,
+ }
+ for r in rows
+ ]
+
+
+@router.get("/signals")
+async def get_signals(request: Request, limit: int = 50):
+ """Get recent signals."""
+ db = request.app.state.db
+ async with db.get_session() as session:
+ stmt = select(SignalRow).order_by(SignalRow.created_at.desc()).limit(limit)
+ result = await session.execute(stmt)
+ rows = result.scalars().all()
+ return [
+ {
+ "id": r.id,
+ "strategy": r.strategy,
+ "symbol": r.symbol,
+ "side": r.side,
+ "price": float(r.price),
+ "quantity": float(r.quantity),
+ "reason": r.reason,
+ "created_at": r.created_at.isoformat() if r.created_at else None,
+ }
+ for r in rows
+ ]
diff --git a/services/api/src/trading_api/routers/portfolio.py b/services/api/src/trading_api/routers/portfolio.py
new file mode 100644
index 0000000..f4169cb
--- /dev/null
+++ b/services/api/src/trading_api/routers/portfolio.py
@@ -0,0 +1,41 @@
+"""Portfolio endpoints."""
+from fastapi import APIRouter, Request
+from shared.sa_models import PositionRow, PortfolioSnapshotRow
+from sqlalchemy import select
+
+router = APIRouter()
+
+
+@router.get("/positions")
+async def get_positions(request: Request):
+ """Get all current positions."""
+ db = request.app.state.db
+ async with db.get_session() as session:
+ result = await session.execute(select(PositionRow))
+ rows = result.scalars().all()
+ return [
+ {
+ "symbol": r.symbol,
+ "quantity": float(r.quantity),
+ "avg_entry_price": float(r.avg_entry_price),
+ "current_price": float(r.current_price),
+ "unrealized_pnl": float(r.quantity * (r.current_price - r.avg_entry_price)),
+ }
+ for r in rows
+ ]
+
+
+@router.get("/snapshots")
+async def get_snapshots(request: Request, days: int = 30):
+ """Get portfolio snapshots for the last N days."""
+ db = request.app.state.db
+ snapshots = await db.get_portfolio_snapshots(days=days)
+ return [
+ {
+ "total_value": float(s["total_value"]),
+ "realized_pnl": float(s["realized_pnl"]),
+ "unrealized_pnl": float(s["unrealized_pnl"]),
+ "snapshot_at": s["snapshot_at"].isoformat(),
+ }
+ for s in snapshots
+ ]
diff --git a/services/api/src/trading_api/routers/strategies.py b/services/api/src/trading_api/routers/strategies.py
new file mode 100644
index 0000000..a8d778d
--- /dev/null
+++ b/services/api/src/trading_api/routers/strategies.py
@@ -0,0 +1,28 @@
+"""Strategy endpoints."""
+import sys
+from pathlib import Path
+
+from fastapi import APIRouter
+
+# Add strategy-engine to path for plugin loading
+_STRATEGY_DIR = Path(__file__).resolve().parents[5] / "strategy-engine"
+if str(_STRATEGY_DIR) not in sys.path:
+ sys.path.insert(0, str(_STRATEGY_DIR))
+
+router = APIRouter()
+
+
+@router.get("/")
+async def list_strategies():
+ """List available strategies."""
+ from strategy_engine.plugin_loader import load_strategies
+ strategies_dir = _STRATEGY_DIR / "strategies"
+ strategies = load_strategies(strategies_dir)
+ return [
+ {
+ "name": s.name,
+ "warmup_period": s.warmup_period,
+ "class": type(s).__name__,
+ }
+ for s in strategies
+ ]
diff --git a/services/api/tests/__init__.py b/services/api/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/services/api/tests/__init__.py
diff --git a/services/api/tests/test_api.py b/services/api/tests/test_api.py
new file mode 100644
index 0000000..99cf9e3
--- /dev/null
+++ b/services/api/tests/test_api.py
@@ -0,0 +1,17 @@
+"""Tests for the REST API."""
+import pytest
+from unittest.mock import AsyncMock, MagicMock, patch
+from fastapi.testclient import TestClient
+
+
+def test_health_endpoint():
+ """Health endpoint returns ok."""
+ from trading_api.main import app
+ # Override lifespan to skip DB
+ with patch("trading_api.main.lifespan") as mock_lifespan:
+ mock_lifespan.return_value.__aenter__ = AsyncMock()
+ mock_lifespan.return_value.__aexit__ = AsyncMock()
+ client = TestClient(app)
+ response = client.get("/health")
+ assert response.status_code == 200
+ assert response.json()["status"] == "ok"