bb3c475f22
Signal daemon (signal_web.py): candle-boundary scanner mirroring telegram_bot.py, publishes serialized SignalReport JSON to Redis. Web API (web/app.py): thin FastAPI reader behind Apache, serves /api/signals REST + /ws WebSocket from Redis pub/sub. Frontend (web/static/index.html): 3-pair grid, color-coded signals, indicator breakdown per timeframe, trade setup display. Also: XAU-specific penalty scoping in signal_aggregator (softer confidence penalties for gold, original strict values for forex), pair-aware session filter routing XAU to its own blocked hours.
122 lines
3.2 KiB
Python
122 lines
3.2 KiB
Python
#!/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")
|