Quasar-Executo / patch_websocket_hub.py
KarlQuant's picture
Upload patch_websocket_hub.py
c10af81 verified
#!/usr/bin/env python3
"""
patch_websocket_hub.py
──────────────────────
Injects TradeLogParser (from hub_dashboard_service.py) and four API routes
into /app/websocket_hub.py so that port 7860 serves:
GET /api/trades β†’ full open + closed state + stats
GET /api/trades/open β†’ open trades only
GET /api/trades/closed β†’ recent closed trades + stats (?limit=N, default 50)
GET /api/health β†’ service health including trade counts
Usage:
python3 patch_websocket_hub.py [--target /app/websocket_hub.py] [--dry-run]
"""
import argparse
import shutil
import sys
from datetime import datetime
from pathlib import Path
# ── Snippet 1: Parser instantiation block ─────────────────────────────────────
# Inserted BEFORE `_START_TIME = time.time()`
PARSER_BLOCK = '''
# ── Trade log parser β€” injected so /api/trades is served on port 7860 ────────
import sys as _sys, os as _os
_sys.path.insert(0, '/app')
from hub_dashboard_service import TradeLogParser as _TradeLogParser
_trade_parser = _TradeLogParser(log_dir=_os.environ.get("RANKER_LOG_DIR", "/app/ranker_logs"))
_trade_parser.start_background()
'''
# ── Snippet 2: FastAPI route definitions ──────────────────────────────────────
# Appended AFTER `_START_TIME = time.time()`
TRADE_ROUTES = '''
# ── /api/trades routes β€” injected by patch_websocket_hub.py ──────────────────
@app.get("/api/trades")
async def api_trades():
"""Full trade state: open trades, recent closed trades, summary stats."""
return JSONResponse(_trade_parser.get_state())
@app.get("/api/trades/open")
async def api_trades_open():
"""Open trades only."""
state = _trade_parser.get_state()
return JSONResponse({"open": state["open"]})
@app.get("/api/trades/closed")
async def api_trades_closed(limit: int = 50):
"""Recent closed trades (newest first) + cumulative stats."""
state = _trade_parser.get_state()
return JSONResponse({
"closed": state["closed"][:limit],
"stats": state["stats"],
})
@app.get("/api/health")
async def api_health():
"""Service health check β€” includes live trade counts and log-file inventory."""
import glob as _glob
return JSONResponse({
"service": "websocket_hub",
"version": "v2.0",
"status": "running",
"log_files": len(_glob.glob("/app/ranker_logs/*.log")),
"trade_open": len(_trade_parser.get_state()["open"]),
"trade_closed": len(_trade_parser.get_state()["closed"]),
})
'''
ANCHOR = "_START_TIME = time.time()"
def patch(source: str) -> str:
"""Apply both substitutions and return the patched source."""
if ANCHOR not in source:
raise ValueError(
f"Anchor '{ANCHOR}' not found in source β€” "
"is this the right file?"
)
# Count occurrences so we can give a clear warning if there are multiples.
count = source.count(ANCHOR)
if count > 1:
print(
f"[WARNING] Anchor appears {count} times; "
"only the first occurrence will be modified.",
file=sys.stderr,
)
# ── Step 1: prepend the parser block before the anchor ────────────────────
src = source.replace(ANCHOR, PARSER_BLOCK + ANCHOR, 1)
# ── Step 2: append trade routes after the (now-unique) anchor ────────────
# After step 1, ANCHOR still appears exactly once (at the end of the
# inserted block), so a second replace(…, 1) is safe.
src = src.replace(ANCHOR, ANCHOR + TRADE_ROUTES, 1)
return src
def main() -> None:
ap = argparse.ArgumentParser(description="Patch websocket_hub.py with trade routes.")
ap.add_argument(
"--target",
default="/app/websocket_hub.py",
help="Path to websocket_hub.py (default: /app/websocket_hub.py)",
)
ap.add_argument(
"--dry-run",
action="store_true",
help="Print the patched source to stdout instead of writing to disk.",
)
args = ap.parse_args()
target = Path(args.target)
if not target.exists():
print(f"[ERROR] Target file not found: {target}", file=sys.stderr)
sys.exit(1)
original = target.read_text(encoding="utf-8")
# Guard: don't double-patch
if "_trade_parser" in original:
print(
"[SKIP] Patch already applied (_trade_parser already present in file).",
file=sys.stderr,
)
sys.exit(0)
patched = patch(original)
if args.dry_run:
print(patched)
return
# ── Backup the original ───────────────────────────────────────────────────
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
backup = target.with_suffix(f".bak_{ts}.py")
shutil.copy2(target, backup)
print(f"[INFO] Backup written β†’ {backup}")
target.write_text(patched, encoding="utf-8")
print(f"[OK] Patch applied β†’ {target}")
print(f" Routes added: /api/trades /api/trades/open /api/trades/closed /api/health")
if __name__ == "__main__":
main()