Add web dashboard (Redis + FastAPI + WebSocket), scope XAU penalties

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.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
harwinder
2026-05-14 23:15:37 -04:00
parent 57769967d1
commit 98daf71a52
10 changed files with 1401 additions and 883 deletions
+121
View File
@@ -0,0 +1,121 @@
#!/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")