#!/usr/bin/env python3 """ ForexBot2 — Web API ==================== Thin FastAPI process that reads signal data from Redis and serves it via REST + WebSocket. Apache reverse-proxies to this. Endpoints: GET /api/signals — current snapshot from Redis WS /ws — real-time updates via Redis pub/sub No signal computation here — signal_web.py daemon handles that. Usage: uvicorn web.app:app --host 127.0.0.1 --port 8000 """ import asyncio import json import logging import os from contextlib import asynccontextmanager from typing import Optional, Set import redis.asyncio as aioredis import uvicorn from fastapi import FastAPI, WebSocket, WebSocketDisconnect from fastapi.middleware.cors import CORSMiddleware logging.basicConfig( format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", level=logging.INFO, datefmt="%H:%M:%S", ) logger = logging.getLogger("web_api") REDIS_URL = os.environ.get("REDIS_URL", "redis://localhost:6379/0") REDIS_KEY = "forexbot2:signals" REDIS_CHANNEL = "forexbot2:updates" _redis: Optional[aioredis.Redis] = None _ws_clients: Set[WebSocket] = set() _subscriber_task: Optional[asyncio.Task] = None async def _redis_subscriber() -> None: pubsub = _redis.pubsub() await pubsub.subscribe(REDIS_CHANNEL) try: async for msg in pubsub.listen(): if msg["type"] != "message": continue payload = msg["data"] dead = [] for ws in _ws_clients: try: await ws.send_text(payload) except Exception: dead.append(ws) for ws in dead: _ws_clients.discard(ws) except asyncio.CancelledError: pass finally: await pubsub.unsubscribe(REDIS_CHANNEL) await pubsub.close() @asynccontextmanager async def lifespan(application: FastAPI): global _redis, _subscriber_task _redis = aioredis.from_url(REDIS_URL, decode_responses=True) await _redis.ping() logger.info(f"Redis connected: {REDIS_URL}") _subscriber_task = asyncio.create_task(_redis_subscriber()) logger.info("Redis subscriber started") yield _subscriber_task.cancel() await asyncio.gather(_subscriber_task, return_exceptions=True) await _redis.close() logger.info("Web API stopped") app = FastAPI(title="ForexBot2 API", lifespan=lifespan) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["GET"], allow_headers=["*"], ) @app.get("/api/signals") async def api_signals(): raw = await _redis.get(REDIS_KEY) if raw: return json.loads(raw) return {"type": "update", "timestamp": "", "data": {}} @app.websocket("/ws") async def ws_endpoint(ws: WebSocket): await ws.accept() _ws_clients.add(ws) try: raw = await _redis.get(REDIS_KEY) if raw: await ws.send_text(raw) while True: await ws.receive_text() except WebSocketDisconnect: pass finally: _ws_clients.discard(ws) if __name__ == "__main__": port = int(os.environ.get("WEB_PORT", "8000")) logger.info(f"Starting ForexBot2 API on http://127.0.0.1:{port}") uvicorn.run(app, host="127.0.0.1", port=port, log_level="info")