KarlQuant commited on
Commit
380417a
Β·
verified Β·
1 Parent(s): f42d35d

Update ranker_logging.py

Browse files
Files changed (1) hide show
  1. ranker_logging.py +355 -56
ranker_logging.py CHANGED
@@ -1,56 +1,355 @@
1
- import logging
2
- import sys
3
- from enum import Enum, auto
4
-
5
- class LogLevel(Enum):
6
- DEBUG = auto()
7
- INFO = auto()
8
- WARNING = auto()
9
- ERROR = auto()
10
- CRITICAL = auto()
11
-
12
- class EventCategory(Enum):
13
- INITIALIZATION = 'INITIALIZATION'
14
- PROCESSING = 'PROCESSING'
15
- TERMINATION = 'TERMINATION'
16
- ERROR_OCCURRED = 'ERROR_OCCURRED'
17
-
18
- class LogEntry:
19
- def __init__(self, level: LogLevel, category: EventCategory, message: str):
20
- self.level = level
21
- self.category = category
22
- self.message = message
23
- self.timestamp = logging.Formatter.formatTime(logging.LogRecord('', 0, '', 0, message, None, None))
24
-
25
- def __str__(self):
26
- return f"[{self.timestamp}] [{self.level.name}] [{self.category.value}] {self.message}"
27
-
28
- class RankerLogger:
29
- def __init__(self, name: str):
30
- self.logger = logging.getLogger(name)
31
- self.logger.setLevel(logging.DEBUG)
32
- ch = logging.StreamHandler(sys.stdout)
33
- ch.setLevel(logging.DEBUG)
34
- formatter = logging.Formatter('%(message)s')
35
- ch.setFormatter(formatter)
36
- self.logger.addHandler(ch)
37
-
38
- def log(self, level: LogLevel, category: EventCategory, message: str):
39
- entry = LogEntry(level, category, message)
40
- if level == LogLevel.DEBUG:
41
- self.logger.debug(str(entry))
42
- elif level == LogLevel.INFO:
43
- self.logger.info(str(entry))
44
- elif level == LogLevel.WARNING:
45
- self.logger.warning(str(entry))
46
- elif level == LogLevel.ERROR:
47
- self.logger.error(str(entry))
48
- elif level == LogLevel.CRITICAL:
49
- self.logger.critical(str(entry))
50
-
51
- class RankerLogBridge:
52
- def __init__(self, ranker_logger: RankerLogger):
53
- self.ranker_logger = ranker_logger
54
-
55
- def log_event(self, level: LogLevel, category: EventCategory, message: str):
56
- self.ranker_logger.log(level, category, message)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ ╔══════════════════════════════════════════════════════════════════════════════════════╗
4
+ β•‘ QUASAR RANKER β€” COMPLETE LOGGING SYSTEM β•‘
5
+ β•‘ ────────────────────────────────────────────────────────────────────────────────── β•‘
6
+ β•‘ Provides file-based logging with JSON export, in-memory buffer, and REST API. β•‘
7
+ β•‘ VERSION: v2.0 | 2026-03-30 β•‘
8
+ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
9
+ """
10
+
11
+ import json
12
+ import logging
13
+ import os
14
+ import sys
15
+ import time
16
+ from collections import deque, defaultdict
17
+ from datetime import datetime
18
+ from enum import Enum, auto
19
+ from pathlib import Path
20
+ from threading import Lock
21
+ from typing import Dict, List, Optional, Any
22
+
23
+
24
+ class LogLevel(Enum):
25
+ DEBUG = auto()
26
+ INFO = auto()
27
+ WARNING = auto()
28
+ ERROR = auto()
29
+ CRITICAL = auto()
30
+
31
+
32
+ class EventCategory(Enum):
33
+ INITIALIZATION = 'INITIALIZATION'
34
+ PROCESSING = 'PROCESSING'
35
+ TERMINATION = 'TERMINATION'
36
+ ERROR_OCCURRED = 'ERROR_OCCURRED'
37
+ CONNECTION = 'CONNECTION'
38
+ TRAINING = 'TRAINING'
39
+ SIGNAL = 'SIGNAL'
40
+ TRADE = 'TRADE'
41
+ RANKING = 'RANKING'
42
+
43
+
44
+ @dataclass
45
+ class LogEntry:
46
+ """Structured log entry with all metadata."""
47
+ timestamp: float
48
+ level: str
49
+ category: str
50
+ message: str
51
+ asset: Optional[str] = None
52
+ metadata: Optional[Dict] = None
53
+
54
+ def to_dict(self) -> dict:
55
+ return {
56
+ "ts": self.timestamp,
57
+ "timestamp": datetime.fromtimestamp(self.timestamp).isoformat(),
58
+ "level": self.level,
59
+ "category": self.category,
60
+ "message": self.message,
61
+ "asset": self.asset,
62
+ "metadata": self.metadata or {},
63
+ }
64
+
65
+ def to_file_line(self) -> str:
66
+ """Format for file logging (human-readable)."""
67
+ dt = datetime.fromtimestamp(self.timestamp).strftime("%Y-%m-%d %H:%M:%S")
68
+ asset_str = f" | {self.asset}" if self.asset else ""
69
+ meta_str = f" | {json.dumps(self.metadata)}" if self.metadata else ""
70
+ return f"[{dt}] | {self.level:8s} | {self.category:15s}{asset_str} | {self.message}{meta_str}"
71
+
72
+
73
+ class RankerLogger:
74
+ """
75
+ Complete logger with file output, in-memory buffer, and JSON export.
76
+
77
+ Features:
78
+ - Writes to both console and rotating log files
79
+ - Maintains in-memory buffer for API queries
80
+ - Per-asset and per-category indexing
81
+ - JSON export for persistence
82
+ - Thread-safe with locks
83
+ """
84
+
85
+ def __init__(
86
+ self,
87
+ name: str = "QuasarAXRVI",
88
+ buffer_size: int = 1000,
89
+ log_dir: str = "./ranker_logs",
90
+ on_event: Optional[callable] = None,
91
+ ):
92
+ self.name = name
93
+ self.buffer_size = buffer_size
94
+ self.log_dir = Path(log_dir)
95
+ self.on_event = on_event
96
+
97
+ # Create log directory if it doesn't exist
98
+ self.log_dir.mkdir(parents=True, exist_ok=True)
99
+
100
+ # In-memory buffers
101
+ self._buffer: deque = deque(maxlen=buffer_size)
102
+ self._by_asset: Dict[str, deque] = defaultdict(lambda: deque(maxlen=buffer_size // 2))
103
+ self._by_level: Dict[str, deque] = defaultdict(lambda: deque(maxlen=buffer_size // 2))
104
+ self._by_category: Dict[str, deque] = defaultdict(lambda: deque(maxlen=buffer_size // 2))
105
+
106
+ self._lock = Lock()
107
+
108
+ # File handler for persistent logging
109
+ self._setup_file_logging()
110
+
111
+ # Console logging
112
+ self.console_logger = logging.getLogger(name)
113
+ self.console_logger.setLevel(logging.DEBUG)
114
+ if not self.console_logger.handlers:
115
+ ch = logging.StreamHandler(sys.stdout)
116
+ ch.setLevel(logging.DEBUG)
117
+ formatter = logging.Formatter('%(message)s')
118
+ ch.setFormatter(formatter)
119
+ self.console_logger.addHandler(ch)
120
+
121
+ self.stats = {
122
+ "total_events": 0,
123
+ "by_level": defaultdict(int),
124
+ "by_category": defaultdict(int),
125
+ "by_asset": defaultdict(int),
126
+ "errors": defaultdict(int),
127
+ }
128
+
129
+ self._log(LogLevel.INFO, EventCategory.INITIALIZATION,
130
+ f"RankerLogger initialized | log_dir={log_dir} | buffer_size={buffer_size}")
131
+
132
+ def _setup_file_logging(self):
133
+ """Setup rotating file logging."""
134
+ log_file = self.log_dir / f"{self.name}_{datetime.now().strftime('%Y%m%d')}.log"
135
+
136
+ file_handler = logging.FileHandler(log_file, encoding='utf-8')
137
+ file_handler.setLevel(logging.DEBUG)
138
+ file_formatter = logging.Formatter('%(message)s')
139
+ file_handler.setFormatter(file_formatter)
140
+
141
+ self.file_logger = logging.getLogger(f"{self.name}_file")
142
+ self.file_logger.setLevel(logging.DEBUG)
143
+ self.file_logger.addHandler(file_handler)
144
+ self.file_logger.propagate = False
145
+
146
+ def _log(
147
+ self,
148
+ level: LogLevel,
149
+ category: EventCategory,
150
+ message: str,
151
+ asset: Optional[str] = None,
152
+ metadata: Optional[Dict] = None,
153
+ ):
154
+ """Internal logging method."""
155
+ entry = LogEntry(
156
+ timestamp=time.time(),
157
+ level=level.name,
158
+ category=category.value,
159
+ message=message,
160
+ asset=asset,
161
+ metadata=metadata,
162
+ )
163
+
164
+ # Write to file
165
+ file_line = entry.to_file_line()
166
+ self.file_logger.info(file_line)
167
+
168
+ # Write to console (simplified)
169
+ console_line = f"{entry.level:8s} | {entry.category:15s}"
170
+ if asset:
171
+ console_line += f" | {asset}"
172
+ console_line += f" | {message}"
173
+ if level == LogLevel.DEBUG:
174
+ self.console_logger.debug(console_line)
175
+ elif level == LogLevel.INFO:
176
+ self.console_logger.info(console_line)
177
+ elif level == LogLevel.WARNING:
178
+ self.console_logger.warning(console_line)
179
+ elif level == LogLevel.ERROR:
180
+ self.console_logger.error(console_line)
181
+ else:
182
+ self.console_logger.critical(console_line)
183
+
184
+ # Store in memory buffers
185
+ with self._lock:
186
+ self._buffer.append(entry)
187
+ if asset:
188
+ self._by_asset[asset].append(entry)
189
+ self._by_level[level.name].append(entry)
190
+ self._by_category[category.value].append(entry)
191
+
192
+ self.stats["total_events"] += 1
193
+ self.stats["by_level"][level.name] += 1
194
+ self.stats["by_category"][category.value] += 1
195
+ if asset:
196
+ self.stats["by_asset"][asset] += 1
197
+ if level == LogLevel.ERROR or level == LogLevel.CRITICAL:
198
+ self.stats["errors"][category.value] += 1
199
+
200
+ # Callback for external consumers (e.g., dashboard)
201
+ if self.on_event:
202
+ try:
203
+ self.on_event(entry.to_dict())
204
+ except Exception:
205
+ pass
206
+
207
+ # ── Public logging methods (called by ranker) ────────────────────────────────
208
+
209
+ def connection_event(self, component: str, status: str, details: str = ""):
210
+ """Log WebSocket or Hub connection events."""
211
+ msg = f"{component} | {status}"
212
+ if details:
213
+ msg += f" | {details}"
214
+ self._log(LogLevel.INFO if status in ["connected", "ready"] else LogLevel.WARNING,
215
+ EventCategory.CONNECTION, msg)
216
+
217
+ def training_update(self, step: int, loss: float, lr: float, asset_count: int = 0):
218
+ """Log training progress."""
219
+ metadata = {"step": step, "loss": loss, "lr": lr, "asset_count": asset_count}
220
+ self._log(LogLevel.DEBUG, EventCategory.TRAINING,
221
+ f"step={step} | loss={loss:.4f} | lr={lr:.6f} | assets={asset_count}",
222
+ metadata=metadata)
223
+
224
+ def hub_update(self, asset: str, avn_accuracy: float, signal_confidence: float):
225
+ """Log hub snapshot updates."""
226
+ metadata = {"avn_accuracy": avn_accuracy, "signal_confidence": signal_confidence}
227
+ self._log(LogLevel.DEBUG, EventCategory.PROCESSING,
228
+ f"hub update | acc={avn_accuracy:.3f} | conf={signal_confidence:.3f}",
229
+ asset=asset, metadata=metadata)
230
+
231
+ def signal(self, asset: str, direction: str, confidence: float, significance: float):
232
+ """Log signal generation."""
233
+ metadata = {"direction": direction, "confidence": confidence, "significance": significance}
234
+ self._log(LogLevel.INFO, EventCategory.SIGNAL,
235
+ f"{direction} | conf={confidence:.3f} | sig={significance:.3f}",
236
+ asset=asset, metadata=metadata)
237
+
238
+ def trade_open(self, trade_id: str, asset: str, direction: str, price: float, qty: float):
239
+ """Log trade opening."""
240
+ metadata = {"trade_id": trade_id, "price": price, "qty": qty}
241
+ self._log(LogLevel.INFO, EventCategory.TRADE,
242
+ f"TRADE OPENED | ID={trade_id} | Dir={direction} | Entry={price:.4f} | Qty={qty:.6f}",
243
+ asset=asset, metadata=metadata)
244
+
245
+ def trade_close(self, trade_id: str, asset: str, pnl: float, return_pct: float):
246
+ """Log trade closing."""
247
+ metadata = {"trade_id": trade_id, "pnl": pnl, "return_pct": return_pct}
248
+ self._log(LogLevel.INFO, EventCategory.TRADE,
249
+ f"TRADE CLOSED | ID={trade_id} | pnl={pnl:+.4f} | return={return_pct:+.2%}",
250
+ asset=asset, metadata=metadata)
251
+
252
+ def ranking_update(self, rankings: List[Dict], top_asset: str, top_score: float):
253
+ """Log ranking cycle results."""
254
+ metadata = {"top_asset": top_asset, "top_score": top_score, "num_ranked": len(rankings)}
255
+ self._log(LogLevel.DEBUG, EventCategory.RANKING,
256
+ f"rankings | top={top_asset} | score={top_score:.4f} | total={len(rankings)}",
257
+ metadata=metadata)
258
+
259
+ # ── Generic log method (backward compatibility) ─────────────────────────────
260
+
261
+ def log(self, level: LogLevel, category: EventCategory, message: str, asset: Optional[str] = None):
262
+ """Generic log method for custom events."""
263
+ self._log(level, category, message, asset=asset)
264
+
265
+ # ── API methods for dashboard ───────────────────────────────────────────────
266
+
267
+ def get_recent(self, n: int = 50, category: Optional[str] = None) -> List[dict]:
268
+ """Get most recent n log entries, optionally filtered by category."""
269
+ with self._lock:
270
+ if category:
271
+ entries = list(self._by_category.get(category, []))
272
+ else:
273
+ entries = list(self._buffer)
274
+ return [e.to_dict() for e in entries[-n:]]
275
+
276
+ def get_by_asset(self, asset: str, n: int = 30) -> List[dict]:
277
+ """Get recent logs for a specific asset."""
278
+ with self._lock:
279
+ entries = list(self._by_asset.get(asset, []))
280
+ return [e.to_dict() for e in entries[-n:]]
281
+
282
+ def get_by_level(self, level: str, n: int = 50) -> List[dict]:
283
+ """Get recent logs by log level."""
284
+ with self._lock:
285
+ entries = list(self._by_level.get(level.upper(), []))
286
+ return [e.to_dict() for e in entries[-n:]]
287
+
288
+ def get_stats(self) -> dict:
289
+ """Get logging statistics."""
290
+ with self._lock:
291
+ return {
292
+ "total_events": self.stats["total_events"],
293
+ "by_level": dict(self.stats["by_level"]),
294
+ "by_category": dict(self.stats["by_category"]),
295
+ "by_asset": dict(self.stats["by_asset"]),
296
+ "errors": dict(self.stats["errors"]),
297
+ "buffer_size": len(self._buffer),
298
+ "buffer_capacity": self.buffer_size,
299
+ }
300
+
301
+ def clear_buffer(self):
302
+ """Clear in-memory buffer."""
303
+ with self._lock:
304
+ self._buffer.clear()
305
+ self._by_asset.clear()
306
+ self._by_level.clear()
307
+ self._by_category.clear()
308
+ self.stats = {
309
+ "total_events": 0,
310
+ "by_level": defaultdict(int),
311
+ "by_category": defaultdict(int),
312
+ "by_asset": defaultdict(int),
313
+ "errors": defaultdict(int),
314
+ }
315
+
316
+ def export_json(self, filepath: str, n: int = 500):
317
+ """Export logs to JSON file."""
318
+ entries = self.get_recent(n)
319
+ with open(filepath, 'w') as f:
320
+ json.dump({
321
+ "export_time": datetime.now().isoformat(),
322
+ "count": len(entries),
323
+ "logs": entries
324
+ }, f, indent=2)
325
+
326
+
327
+ class RankerLogBridge:
328
+ """
329
+ Bridge between ranker components and the logging system.
330
+ Provides convenience methods for common logging patterns.
331
+ """
332
+
333
+ def __init__(self, ranker_logger: RankerLogger):
334
+ self.logger = ranker_logger
335
+
336
+ def capture_signal(self, asset: str, buffer, score: float):
337
+ """Capture signal generation from asset buffer."""
338
+ if buffer and buffer.latest_signal:
339
+ action = buffer.latest_signal.get("action", "HOLD")
340
+ confidence = buffer.latest_signal.get("confidence", 0.0)
341
+ self.logger.signal(asset, action, confidence, score)
342
+
343
+ def capture_ranking(self, ranked: List, hub_snapshots: Dict):
344
+ """Capture ranking results."""
345
+ if ranked:
346
+ top = ranked[0]
347
+ self.logger.ranking_update(
348
+ [r.space_name for r in ranked[:5]],
349
+ top.space_name,
350
+ top.final_priority
351
+ )
352
+
353
+ def log_event(self, level: LogLevel, category: EventCategory, message: str, asset: Optional[str] = None):
354
+ """Generic event logging."""
355
+ self.logger.log(level, category, message, asset=asset)