RealComp commited on
Commit
6196022
·
verified ·
1 Parent(s): 798ba03

Upload icpe_ingest_relay.py

Browse files
Files changed (1) hide show
  1. icpe_ingest_relay.py +219 -219
icpe_ingest_relay.py CHANGED
@@ -1,219 +1,219 @@
1
- #!/usr/bin/env python3
2
- """
3
- ╔══════════════════════════════════════════════════════════════════════════════╗
4
- ║ K1RL QUASAR — ICPE Ingest Relay v1.1 (HARDCODED — TESTING ONLY) ║
5
- ║ (icpe_ingest_relay.py) ║
6
- ║ Asset: V75 ║
7
- ╠══════════════════════════════════════════════════════════════════════════════╣
8
- ║ WHAT IT DOES ║
9
- ║ Sits inside telegram_notifier.py and intercepts every valid signal ║
10
- ║ from _on_signal() — flips AND non-flips — then POSTs the full ║
11
- ║ structured JSON payload to RICPE /ingest on the main PIL space. ║
12
- ║ ║
13
- ║ ⚠️ TESTING-ONLY BUILD ║
14
- ║ ICPE_INGEST_URL, INTERNAL_API_KEY, and ASSET_SYMBOL are HARDCODED ║
15
- ║ below instead of read from HF Space Secrets. HF Secrets still ║
16
- ║ override these if present, so this is safe to leave in place — but ║
17
- ║ revert to the Secrets-driven build before relying on this long-term, ║
18
- ║ since hardcoded URLs/keys won't survive a space rename or key rotation. ║
19
- ║ ║
20
- ║ INTEGRATION INTO telegram_notifier.py (4 surgical edits) ║
21
- ║ 1. [top of file] from icpe_ingest_relay import IcpeRelay ║
22
- ║ 2. [__init__] self._icpe = IcpeRelay() ║
23
- ║ 3. [_on_signal] self._icpe.push(...) ║
24
- ║ 4. [finally] self._icpe.shutdown() ║
25
- ║ ║
26
- ║ DATA FLOW ║
27
- ║ Redis → _on_signal() → IcpeRelay.push() ║
28
- ║ → ThreadPool → POST https://karlcon-portfolio-intelligence.hf.space/ricpe/ingest ║
29
- ║ → RICPE ContextStore ║
30
- ║ → PIL GET /context/{sym} ║
31
- ╚══════════════════════════════════════════════════════════════════════════════╝
32
- """
33
-
34
- import logging
35
- import os
36
- import time
37
- from concurrent.futures import ThreadPoolExecutor
38
- from datetime import datetime, timezone, timedelta
39
- from typing import Any, Dict, Optional
40
-
41
- import requests
42
-
43
- log = logging.getLogger("QUASAR.ICPE.Relay")
44
-
45
- # ────────────────────────────────────────────────────────────────────────────
46
- # Config — HARDCODED for testing. HF Secrets (if set) still take priority.
47
- # ─────────────────────────────────────────────────────────────────────────────
48
- _HARDCODED_URL = "https://karlcon-portfolio-intelligence.hf.space/ricpe"
49
- _HARDCODED_API_KEY = "" # ← leave blank: RICPE has no INTERNAL_API_KEY set yet
50
- _HARDCODED_SYMBOL = "V75"
51
-
52
- _BASE_URL = os.environ.get("ICPE_INGEST_URL", _HARDCODED_URL).rstrip("/")
53
- _API_KEY = os.environ.get("INTERNAL_API_KEY", _HARDCODED_API_KEY)
54
- _SYM_ENV = os.environ.get("ASSET_SYMBOL", _HARDCODED_SYMBOL).strip().upper()
55
-
56
- _CAT = timezone(timedelta(hours=2))
57
-
58
- # ─────────────────────────────────────────────────────────────────────────────
59
- # IcpeRelay
60
- # ─────────────────────────────────────────────────────────────────────────────
61
-
62
- class IcpeRelay:
63
- """
64
- Non-blocking ICPE ingest relay.
65
-
66
- One instance per telegram_notifier process. Fires a POST to RICPE /ingest
67
- for every valid signal received from Redis — the RICPE ContextStore uses the
68
- full stream (not just flips) to compute:
69
- • flip_rate_per_min
70
- • signal_frequency_hz
71
- • momentum_bias
72
- • directional_stability
73
- which are injected into the quasar_pil.py LLM reasoning chain via
74
- GET /context/{symbol}.
75
-
76
- Thread-safety: push() is safe to call from the Redis listener thread
77
- (same as send_telegram). The thread pool is isolated; it never touches
78
- the asyncio event loop.
79
- """
80
-
81
- def __init__(self, symbol: Optional[str] = None):
82
- self.symbol = (symbol or _SYM_ENV or "UNKNOWN").upper()
83
- self.url = _BASE_URL # hardcoded above, env overrides if set
84
- self.api_key = _API_KEY
85
- self._pool = ThreadPoolExecutor(max_workers=2, thread_name_prefix="icpe")
86
- self._ok: int = 0
87
- self._fail: int = 0
88
-
89
- if not self.url:
90
- log.warning(
91
- "[IcpeRelay] ICPE_INGEST_URL not set — relay DISABLED.\n"
92
- " Set ICPE_INGEST_URL in HF Space Secrets to enable."
93
- )
94
- else:
95
- log.info(f"[IcpeRelay] {self.symbol} ──► {self.url}/ingest (hardcoded build)")
96
-
97
- # ── Public API ────────────────────────────────────────────────────────────
98
-
99
- def push(
100
- self,
101
- *,
102
- action: str,
103
- price: float,
104
- prev_action: str = "",
105
- is_flip: bool = False,
106
- flip_number: int = 0,
107
- agent: str = "",
108
- keys: int = 1,
109
- total_signals: int = 0,
110
- session_start: float = 0.0,
111
- raw_data: Optional[Dict] = None,
112
- ) -> None:
113
- """
114
- Build the RICPE-compatible payload from every valid _on_signal() event
115
- and submit it to the thread pool. Returns immediately — never blocks
116
- the Redis listener thread.
117
-
118
- Sends for EVERY signal (flip AND non-flip) so RICPE builds the full
119
- momentum and frequency picture for the PIL reasoning chain.
120
- """
121
- if not self.url:
122
- return
123
-
124
- now = time.time()
125
- payload: Dict[str, Any] = {
126
- # ── RICPE /ingest required fields ─────────────────────────────
127
- "symbol": self.symbol,
128
- "action": action.upper(),
129
- "price": round(float(price), 8),
130
- # ── RICPE /ingest optional enrichment ─────────────────────────
131
- "is_flip": bool(is_flip),
132
- "prev_action": prev_action.upper() if prev_action else "",
133
- "flip_number": int(flip_number),
134
- "agent": agent.upper() if agent else "",
135
- "keys": int(keys),
136
- # ── Session context — stored in raw_payload on RICPE ──────────
137
- "total_signals": int(total_signals),
138
- "session_min": round((now - session_start) / 60, 2) if session_start else 0.0,
139
- # ── Timestamps ────────────────────────────────────────────────
140
- "ts_unix": now,
141
- "ts_cat": datetime.now(_CAT).strftime("%H:%M:%S CAT"),
142
- # ── Full raw Redis payload ─────────────────────────────────────
143
- # Stored by RICPE in SignalEvent.raw_payload for PIL to inspect.
144
- "raw": raw_data or {},
145
- }
146
- self._pool.submit(self._post, payload)
147
-
148
- def stats(self) -> Dict[str, Any]:
149
- """Return live counters — useful to expose on a /health endpoint."""
150
- return {
151
- "symbol": self.symbol,
152
- "icpe_url": self.url or "(not configured)",
153
- "sends_ok": self._ok,
154
- "sends_fail": self._fail,
155
- "enabled": bool(self.url),
156
- }
157
-
158
- def shutdown(self) -> None:
159
- """Call from the notifier's finally block."""
160
- self._pool.shutdown(wait=False)
161
- log.info(f"[IcpeRelay] Shutdown — ok={self._ok} fail={self._fail}")
162
-
163
- # ── Internal ──────────────────────────────────────────────────────────────
164
-
165
- def _post(self, payload: Dict[str, Any]) -> bool:
166
- """
167
- Synchronous HTTP POST — runs inside the thread pool, NOT the event loop.
168
-
169
- Retry policy:
170
- • Retry once after 1.5 s on connection / timeout errors.
171
- • Never retry 4xx (bad payload) — avoids hammering RICPE with a
172
- malformed body.
173
- • Silently drops after two failed attempts; trading logic is unaffected.
174
- """
175
- url = self.url + "/ingest"
176
- headers = {"Content-Type": "application/json"}
177
- if self.api_key:
178
- headers["x-api-key"] = self.api_key
179
-
180
- for attempt in range(2):
181
- try:
182
- resp = requests.post(url, json=payload, headers=headers, timeout=8)
183
-
184
- if resp.status_code == 200:
185
- self._ok += 1
186
- log.debug(
187
- f"[IcpeRelay] ✅ {self.symbol} "
188
- f"{payload.get('action')} @ {payload.get('price')} "
189
- f"flip={payload.get('is_flip')}"
190
- )
191
- return True
192
-
193
- # Bad payload — don't retry
194
- if resp.status_code in (400, 422):
195
- self._fail += 1
196
- log.warning(
197
- f"[IcpeRelay] ⚠️ Bad payload "
198
- f"({resp.status_code}): {resp.text[:200]}"
199
- )
200
- return False
201
-
202
- # Other HTTP error — retry once
203
- log.warning(
204
- f"[IcpeRelay] HTTP {resp.status_code} (attempt {attempt + 1})"
205
- )
206
-
207
- except requests.exceptions.ConnectionError as exc:
208
- log.warning(f"[IcpeRelay] Connection error (attempt {attempt + 1}): {exc}")
209
- except requests.exceptions.Timeout:
210
- log.warning(f"[IcpeRelay] Timeout 8 s (attempt {attempt + 1})")
211
- except Exception as exc:
212
- log.error(f"[IcpeRelay] ❌ Unexpected error: {exc}")
213
- break # don't retry unknown errors
214
-
215
- if attempt == 0:
216
- time.sleep(1.5) # brief wait before retry
217
-
218
- self._fail += 1
219
- return False
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ ╔══════════════════════════════════════════════════════════════════════════════╗
4
+ ║ K1RL QUASAR — ICPE Ingest Relay v1.1 (HARDCODED — TESTING ONLY) ║
5
+ ║ (icpe_ingest_relay.py) ║
6
+ ║ Asset: V75 ║
7
+ ╠══════════════════════════════════════════════════════════════════════════════╣
8
+ ║ WHAT IT DOES ║
9
+ ║ Sits inside telegram_notifier.py and intercepts every valid signal ║
10
+ ║ from _on_signal() — flips AND non-flips — then POSTs the full ║
11
+ ║ structured JSON payload to RICPE /ingest on the main PIL space. ║
12
+ ║ ║
13
+ ║ ⚠️ TESTING-ONLY BUILD ║
14
+ ║ ICPE_INGEST_URL, INTERNAL_API_KEY, and ASSET_SYMBOL are HARDCODED ║
15
+ ║ below instead of read from HF Space Secrets. HF Secrets still ║
16
+ ║ override these if present, so this is safe to leave in place — but ║
17
+ ║ revert to the Secrets-driven build before relying on this long-term, ║
18
+ ║ since hardcoded URLs/keys won't survive a space rename or key rotation. ║
19
+ ║ ║
20
+ ║ INTEGRATION INTO telegram_notifier.py (4 surgical edits) ║
21
+ ║ 1. [top of file] from icpe_ingest_relay import IcpeRelay ║
22
+ ║ 2. [__init__] self._icpe = IcpeRelay() ║
23
+ ║ 3. [_on_signal] self._icpe.push(...) ║
24
+ ║ 4. [finally] self._icpe.shutdown() ║
25
+ ║ ║
26
+ ║ DATA FLOW ║
27
+ ║ Redis → _on_signal() → IcpeRelay.push() ║
28
+ ║ → ThreadPool → POST https://q-karl-portfolio-intelligence.hf.space/ricpe/ingest ║
29
+ ║ → RICPE ContextStore ║
30
+ ║ → PIL GET /context/{sym} ║
31
+ ╚══════════════════════════════════════════════════════════════════════════════╝
32
+ """
33
+
34
+ import logging
35
+ import os
36
+ import time
37
+ from concurrent.futures import ThreadPoolExecutor
38
+ from datetime import datetime, timezone, timedelta
39
+ from typing import Any, Dict, Optional
40
+
41
+ import requests
42
+
43
+ log = logging.getLogger("QUASAR.ICPE.Relay")
44
+
45
+ # ────────────────────────���────────────────────────────────────────────────────
46
+ # Config — HARDCODED for testing. HF Secrets (if set) still take priority.
47
+ # ─────────────────────────────────────────────────────────────────────────────
48
+ _HARDCODED_URL = "https://q-karl-portfolio-intelligence.hf.space/ricpe"
49
+ _HARDCODED_API_KEY = "" # ← leave blank: RICPE has no INTERNAL_API_KEY set yet
50
+ _HARDCODED_SYMBOL = "V75"
51
+
52
+ _BASE_URL = os.environ.get("ICPE_INGEST_URL", _HARDCODED_URL).rstrip("/")
53
+ _API_KEY = os.environ.get("INTERNAL_API_KEY", _HARDCODED_API_KEY)
54
+ _SYM_ENV = os.environ.get("ASSET_SYMBOL", _HARDCODED_SYMBOL).strip().upper()
55
+
56
+ _CAT = timezone(timedelta(hours=2))
57
+
58
+ # ─────────────────────────────────────────────────────────────────────────────
59
+ # IcpeRelay
60
+ # ─────────────────────────────────────────────────────────────────────────────
61
+
62
+ class IcpeRelay:
63
+ """
64
+ Non-blocking ICPE ingest relay.
65
+
66
+ One instance per telegram_notifier process. Fires a POST to RICPE /ingest
67
+ for every valid signal received from Redis — the RICPE ContextStore uses the
68
+ full stream (not just flips) to compute:
69
+ • flip_rate_per_min
70
+ • signal_frequency_hz
71
+ • momentum_bias
72
+ • directional_stability
73
+ which are injected into the quasar_pil.py LLM reasoning chain via
74
+ GET /context/{symbol}.
75
+
76
+ Thread-safety: push() is safe to call from the Redis listener thread
77
+ (same as send_telegram). The thread pool is isolated; it never touches
78
+ the asyncio event loop.
79
+ """
80
+
81
+ def __init__(self, symbol: Optional[str] = None):
82
+ self.symbol = (symbol or _SYM_ENV or "UNKNOWN").upper()
83
+ self.url = _BASE_URL # hardcoded above, env overrides if set
84
+ self.api_key = _API_KEY
85
+ self._pool = ThreadPoolExecutor(max_workers=2, thread_name_prefix="icpe")
86
+ self._ok: int = 0
87
+ self._fail: int = 0
88
+
89
+ if not self.url:
90
+ log.warning(
91
+ "[IcpeRelay] ICPE_INGEST_URL not set — relay DISABLED.\n"
92
+ " Set ICPE_INGEST_URL in HF Space Secrets to enable."
93
+ )
94
+ else:
95
+ log.info(f"[IcpeRelay] {self.symbol} ──► {self.url}/ingest (hardcoded build)")
96
+
97
+ # ── Public API ────────────────────────────────────────────────────────────
98
+
99
+ def push(
100
+ self,
101
+ *,
102
+ action: str,
103
+ price: float,
104
+ prev_action: str = "",
105
+ is_flip: bool = False,
106
+ flip_number: int = 0,
107
+ agent: str = "",
108
+ keys: int = 1,
109
+ total_signals: int = 0,
110
+ session_start: float = 0.0,
111
+ raw_data: Optional[Dict] = None,
112
+ ) -> None:
113
+ """
114
+ Build the RICPE-compatible payload from every valid _on_signal() event
115
+ and submit it to the thread pool. Returns immediately — never blocks
116
+ the Redis listener thread.
117
+
118
+ Sends for EVERY signal (flip AND non-flip) so RICPE builds the full
119
+ momentum and frequency picture for the PIL reasoning chain.
120
+ """
121
+ if not self.url:
122
+ return
123
+
124
+ now = time.time()
125
+ payload: Dict[str, Any] = {
126
+ # ── RICPE /ingest required fields ─────────────────────────────
127
+ "symbol": self.symbol,
128
+ "action": action.upper(),
129
+ "price": round(float(price), 8),
130
+ # ── RICPE /ingest optional enrichment ─────────────────────────
131
+ "is_flip": bool(is_flip),
132
+ "prev_action": prev_action.upper() if prev_action else "",
133
+ "flip_number": int(flip_number),
134
+ "agent": agent.upper() if agent else "",
135
+ "keys": int(keys),
136
+ # ── Session context — stored in raw_payload on RICPE ──────────
137
+ "total_signals": int(total_signals),
138
+ "session_min": round((now - session_start) / 60, 2) if session_start else 0.0,
139
+ # ── Timestamps ────────────────────────────────────────────────
140
+ "ts_unix": now,
141
+ "ts_cat": datetime.now(_CAT).strftime("%H:%M:%S CAT"),
142
+ # ── Full raw Redis payload ─────────────────────────────────────
143
+ # Stored by RICPE in SignalEvent.raw_payload for PIL to inspect.
144
+ "raw": raw_data or {},
145
+ }
146
+ self._pool.submit(self._post, payload)
147
+
148
+ def stats(self) -> Dict[str, Any]:
149
+ """Return live counters — useful to expose on a /health endpoint."""
150
+ return {
151
+ "symbol": self.symbol,
152
+ "icpe_url": self.url or "(not configured)",
153
+ "sends_ok": self._ok,
154
+ "sends_fail": self._fail,
155
+ "enabled": bool(self.url),
156
+ }
157
+
158
+ def shutdown(self) -> None:
159
+ """Call from the notifier's finally block."""
160
+ self._pool.shutdown(wait=False)
161
+ log.info(f"[IcpeRelay] Shutdown — ok={self._ok} fail={self._fail}")
162
+
163
+ # ── Internal ──────────────────────────────────────────────────────────────
164
+
165
+ def _post(self, payload: Dict[str, Any]) -> bool:
166
+ """
167
+ Synchronous HTTP POST — runs inside the thread pool, NOT the event loop.
168
+
169
+ Retry policy:
170
+ • Retry once after 1.5 s on connection / timeout errors.
171
+ • Never retry 4xx (bad payload) — avoids hammering RICPE with a
172
+ malformed body.
173
+ • Silently drops after two failed attempts; trading logic is unaffected.
174
+ """
175
+ url = self.url + "/ingest"
176
+ headers = {"Content-Type": "application/json"}
177
+ if self.api_key:
178
+ headers["x-api-key"] = self.api_key
179
+
180
+ for attempt in range(2):
181
+ try:
182
+ resp = requests.post(url, json=payload, headers=headers, timeout=8)
183
+
184
+ if resp.status_code == 200:
185
+ self._ok += 1
186
+ log.debug(
187
+ f"[IcpeRelay] ✅ {self.symbol} "
188
+ f"{payload.get('action')} @ {payload.get('price')} "
189
+ f"flip={payload.get('is_flip')}"
190
+ )
191
+ return True
192
+
193
+ # Bad payload — don't retry
194
+ if resp.status_code in (400, 422):
195
+ self._fail += 1
196
+ log.warning(
197
+ f"[IcpeRelay] ⚠️ Bad payload "
198
+ f"({resp.status_code}): {resp.text[:200]}"
199
+ )
200
+ return False
201
+
202
+ # Other HTTP error — retry once
203
+ log.warning(
204
+ f"[IcpeRelay] HTTP {resp.status_code} (attempt {attempt + 1})"
205
+ )
206
+
207
+ except requests.exceptions.ConnectionError as exc:
208
+ log.warning(f"[IcpeRelay] Connection error (attempt {attempt + 1}): {exc}")
209
+ except requests.exceptions.Timeout:
210
+ log.warning(f"[IcpeRelay] Timeout 8 s (attempt {attempt + 1})")
211
+ except Exception as exc:
212
+ log.error(f"[IcpeRelay] ❌ Unexpected error: {exc}")
213
+ break # don't retry unknown errors
214
+
215
+ if attempt == 0:
216
+ time.sleep(1.5) # brief wait before retry
217
+
218
+ self._fail += 1
219
+ return False