trading/services/api_gateway/main.py
Viktor Barzin 82dc622544 feat(kevin): reconcile Alpaca bracket auto-closes + order status
Bracket stop-loss/take-profit legs fill at Alpaca without passing through the executor, so those closes (and their P&L) were invisible locally.

- broker: add get_order(nested) + list_orders to BaseBroker/AlpacaBroker (+ SimulatedBroker); BrokerOrder carries child legs

- Trade gains broker_order_id (migration f6a7b8c9d0e1); executor stamps the entry order id

- new api_gateway trade-reconcile loop: books a closing SELL + realized P&L when a bracket leg fills (idempotent on the leg order id), syncs PENDING->terminal status, logs drift; runs alongside portfolio_sync

[ci skip]

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 22:31:24 +00:00

146 lines
4.5 KiB
Python

"""FastAPI application — API Gateway for the trading bot."""
from __future__ import annotations
import asyncio
import logging
from contextlib import asynccontextmanager
from typing import AsyncIterator
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from redis.asyncio import Redis
from services.api_gateway.auth.routes import router as auth_router
from services.api_gateway.config import ApiGatewayConfig
from shared.db import create_db
logger = logging.getLogger(__name__)
def create_app(config: ApiGatewayConfig | None = None) -> FastAPI:
"""Build and configure the FastAPI application.
Parameters
----------
config:
Optional config override (useful for testing). If ``None``, a new
:class:`ApiGatewayConfig` is created from environment variables.
"""
if config is None:
config = ApiGatewayConfig()
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
"""Start-up / shutdown hook — connect DB and Redis."""
# Database
engine, session_factory = create_db(config)
app.state.db_engine = engine
app.state.db_session_factory = session_factory
# Redis
app.state.redis = Redis.from_url(
config.redis_url, decode_responses=True
)
app.state.config = config
# Start portfolio sync background task
from services.api_gateway.tasks.portfolio_sync import portfolio_sync_loop
from services.api_gateway.tasks.trade_reconcile import trade_reconcile_loop
sync_task = asyncio.create_task(
portfolio_sync_loop(config, session_factory)
)
reconcile_task = asyncio.create_task(
trade_reconcile_loop(config, session_factory)
)
logger.info("API Gateway started")
yield
# Cancel the background tasks
sync_task.cancel()
reconcile_task.cancel()
for task in (sync_task, reconcile_task):
try:
await task
except asyncio.CancelledError:
pass
# Cleanup
await app.state.redis.aclose()
await engine.dispose()
logger.info("API Gateway stopped")
app = FastAPI(
title="Trading Bot API",
version="0.1.0",
lifespan=lifespan,
)
# CORS
app.add_middleware(
CORSMiddleware,
allow_origins=config.cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Auth routes (unauthenticated)
app.include_router(auth_router)
# Trading routes (authenticated) — imported lazily to avoid circular deps
from services.api_gateway.routes.portfolio import router as portfolio_router
from services.api_gateway.routes.trades import router as trades_router
from services.api_gateway.routes.signals import router as signals_router
from services.api_gateway.routes.strategies import router as strategies_router
from services.api_gateway.routes.news import router as news_router
from services.api_gateway.routes.controls import router as controls_router
from services.api_gateway.routes.backtest import router as backtest_router
from services.api_gateway.routes.meet_kevin import router as meet_kevin_router
from services.api_gateway.routes.meet_kevin_backtest import (
router as meet_kevin_backtest_router,
)
from services.api_gateway.routes.meet_kevin_strategy import (
router as meet_kevin_strategy_router,
)
app.include_router(portfolio_router)
app.include_router(trades_router)
app.include_router(signals_router)
app.include_router(strategies_router)
app.include_router(news_router)
app.include_router(controls_router)
app.include_router(backtest_router)
app.include_router(meet_kevin_router)
app.include_router(meet_kevin_backtest_router)
app.include_router(meet_kevin_strategy_router)
# WebSocket
from services.api_gateway.ws import router as ws_router
app.include_router(ws_router)
# Health check
@app.get("/health", tags=["health"])
async def health() -> dict:
return {"status": "ok"}
return app
def get_app() -> FastAPI:
"""Lazy app factory for uvicorn: ``uvicorn services.api_gateway.main:get_app --factory``."""
return create_app()
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"services.api_gateway.main:get_app",
factory=True,
host="0.0.0.0",
port=8000,
)