KarlQuant commited on
Commit
c10af81
Β·
verified Β·
1 Parent(s): fe3400b

Upload patch_websocket_hub.py

Browse files
Files changed (1) hide show
  1. patch_websocket_hub.py +155 -0
patch_websocket_hub.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ patch_websocket_hub.py
4
+ ──────────────────────
5
+ Injects TradeLogParser (from hub_dashboard_service.py) and four API routes
6
+ into /app/websocket_hub.py so that port 7860 serves:
7
+
8
+ GET /api/trades β†’ full open + closed state + stats
9
+ GET /api/trades/open β†’ open trades only
10
+ GET /api/trades/closed β†’ recent closed trades + stats (?limit=N, default 50)
11
+ GET /api/health β†’ service health including trade counts
12
+
13
+ Usage:
14
+ python3 patch_websocket_hub.py [--target /app/websocket_hub.py] [--dry-run]
15
+ """
16
+
17
+ import argparse
18
+ import shutil
19
+ import sys
20
+ from datetime import datetime
21
+ from pathlib import Path
22
+
23
+ # ── Snippet 1: Parser instantiation block ─────────────────────────────────────
24
+ # Inserted BEFORE `_START_TIME = time.time()`
25
+ PARSER_BLOCK = '''
26
+ # ── Trade log parser β€” injected so /api/trades is served on port 7860 ────────
27
+ import sys as _sys, os as _os
28
+ _sys.path.insert(0, '/app')
29
+ from hub_dashboard_service import TradeLogParser as _TradeLogParser
30
+ _trade_parser = _TradeLogParser(log_dir=_os.environ.get("RANKER_LOG_DIR", "/app/ranker_logs"))
31
+ _trade_parser.start_background()
32
+ '''
33
+
34
+ # ── Snippet 2: FastAPI route definitions ──────────────────────────────────────
35
+ # Appended AFTER `_START_TIME = time.time()`
36
+ TRADE_ROUTES = '''
37
+
38
+ # ── /api/trades routes β€” injected by patch_websocket_hub.py ──────────────────
39
+ @app.get("/api/trades")
40
+ async def api_trades():
41
+ """Full trade state: open trades, recent closed trades, summary stats."""
42
+ return JSONResponse(_trade_parser.get_state())
43
+
44
+
45
+ @app.get("/api/trades/open")
46
+ async def api_trades_open():
47
+ """Open trades only."""
48
+ state = _trade_parser.get_state()
49
+ return JSONResponse({"open": state["open"]})
50
+
51
+
52
+ @app.get("/api/trades/closed")
53
+ async def api_trades_closed(limit: int = 50):
54
+ """Recent closed trades (newest first) + cumulative stats."""
55
+ state = _trade_parser.get_state()
56
+ return JSONResponse({
57
+ "closed": state["closed"][:limit],
58
+ "stats": state["stats"],
59
+ })
60
+
61
+
62
+ @app.get("/api/health")
63
+ async def api_health():
64
+ """Service health check β€” includes live trade counts and log-file inventory."""
65
+ import glob as _glob
66
+ return JSONResponse({
67
+ "service": "websocket_hub",
68
+ "version": "v2.0",
69
+ "status": "running",
70
+ "log_files": len(_glob.glob("/app/ranker_logs/*.log")),
71
+ "trade_open": len(_trade_parser.get_state()["open"]),
72
+ "trade_closed": len(_trade_parser.get_state()["closed"]),
73
+ })
74
+ '''
75
+
76
+ ANCHOR = "_START_TIME = time.time()"
77
+
78
+
79
+ def patch(source: str) -> str:
80
+ """Apply both substitutions and return the patched source."""
81
+ if ANCHOR not in source:
82
+ raise ValueError(
83
+ f"Anchor '{ANCHOR}' not found in source β€” "
84
+ "is this the right file?"
85
+ )
86
+
87
+ # Count occurrences so we can give a clear warning if there are multiples.
88
+ count = source.count(ANCHOR)
89
+ if count > 1:
90
+ print(
91
+ f"[WARNING] Anchor appears {count} times; "
92
+ "only the first occurrence will be modified.",
93
+ file=sys.stderr,
94
+ )
95
+
96
+ # ── Step 1: prepend the parser block before the anchor ────────────────────
97
+ src = source.replace(ANCHOR, PARSER_BLOCK + ANCHOR, 1)
98
+
99
+ # ── Step 2: append trade routes after the (now-unique) anchor ────────────
100
+ # After step 1, ANCHOR still appears exactly once (at the end of the
101
+ # inserted block), so a second replace(…, 1) is safe.
102
+ src = src.replace(ANCHOR, ANCHOR + TRADE_ROUTES, 1)
103
+
104
+ return src
105
+
106
+
107
+ def main() -> None:
108
+ ap = argparse.ArgumentParser(description="Patch websocket_hub.py with trade routes.")
109
+ ap.add_argument(
110
+ "--target",
111
+ default="/app/websocket_hub.py",
112
+ help="Path to websocket_hub.py (default: /app/websocket_hub.py)",
113
+ )
114
+ ap.add_argument(
115
+ "--dry-run",
116
+ action="store_true",
117
+ help="Print the patched source to stdout instead of writing to disk.",
118
+ )
119
+ args = ap.parse_args()
120
+
121
+ target = Path(args.target)
122
+
123
+ if not target.exists():
124
+ print(f"[ERROR] Target file not found: {target}", file=sys.stderr)
125
+ sys.exit(1)
126
+
127
+ original = target.read_text(encoding="utf-8")
128
+
129
+ # Guard: don't double-patch
130
+ if "_trade_parser" in original:
131
+ print(
132
+ "[SKIP] Patch already applied (_trade_parser already present in file).",
133
+ file=sys.stderr,
134
+ )
135
+ sys.exit(0)
136
+
137
+ patched = patch(original)
138
+
139
+ if args.dry_run:
140
+ print(patched)
141
+ return
142
+
143
+ # ── Backup the original ───────────────────────────────────────────────────
144
+ ts = datetime.now().strftime("%Y%m%d_%H%M%S")
145
+ backup = target.with_suffix(f".bak_{ts}.py")
146
+ shutil.copy2(target, backup)
147
+ print(f"[INFO] Backup written β†’ {backup}")
148
+
149
+ target.write_text(patched, encoding="utf-8")
150
+ print(f"[OK] Patch applied β†’ {target}")
151
+ print(f" Routes added: /api/trades /api/trades/open /api/trades/closed /api/health")
152
+
153
+
154
+ if __name__ == "__main__":
155
+ main()