tomirisg25 commited on
Commit
7b272e6
·
verified ·
1 Parent(s): 02316b8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +192 -35
app.py CHANGED
@@ -21,7 +21,7 @@ for module_name, pip_name in REQUIRED_PACKAGES.items():
21
  print(f"✅ {pip_name} установлен!")
22
 
23
  # ============================================
24
- # 👑 TOMIRIS SPACE 29 v2.1 — MACRO SURPRISE ENGINE (Hub-Connected)
25
  # ============================================
26
  import os, time, json, logging, asyncio
27
  from typing import Dict, Any, List, Optional
@@ -41,14 +41,28 @@ HUB_URL = os.getenv("SPACE17_URL", "https://tomiris-ai-name5-5.hf.space")
41
  FRED_KEY = os.getenv("FRED_KEY", "faa11c8e2e4beee08c5b966e8b63a513")
42
  NEWSAPI_KEY = os.getenv("NEWSAPI_KEY", "948c7816beea47baa23b054592472d0e")
43
 
 
 
 
44
  # Консенсус-прогнозы (обновлять ежемесячно)
45
  CONSENSUS = {
46
- "CPI_YOY": 3.2, "CORE_CPI_YOY": 3.5, "UNEMPLOYMENT": 4.0, "NFP": 180000,
47
- "GDP_QOQ": 2.0, "FED_RATE": 4.25, "ISM_MANUF": 49.0, "ISM_SERVICES": 52.0,
48
- "RETAIL_SALES": 0.3, "DURABLE_GOODS": 0.5
 
 
 
 
 
 
 
 
 
 
 
 
49
  }
50
 
51
- CACHE_TTL = {"fred": 3600, "news": 1800}
52
  HISTORY_FILE = "surprise_history.json"
53
 
54
  # ================= HTTP КЛИЕНТ =================
@@ -77,22 +91,30 @@ async def fetch_fred_series(series_id: str, months: int = 13) -> List[Dict]:
77
  cache_key = f"fred_{series_id}_{months}"
78
  if cache_key in cache_store and time.time() - cache_times.get(cache_key, 0) < CACHE_TTL["fred"]:
79
  return cache_store[cache_key]
 
80
  try:
81
  r = await http_client.get(
82
- f"https://api.stlouisfed.org/fred/series/observations?series_id={series_id}&api_key={FRED_KEY}&file_type=json&sort_order=desc&limit={months}")
 
83
  if r.status_code == 200:
84
  data = r.json()
85
- values = [{'date': obs['date'], 'value': float(obs['value'])} for obs in data.get('observations', []) if obs['value'] != '.']
 
 
 
 
86
  cache_store[cache_key] = values
87
  cache_times[cache_key] = time.time()
88
  return values
89
  except Exception as e:
90
  logger.warning(f"FRED {series_id}: {e}")
 
91
  return []
92
 
93
  def get_yoy_change(data: List[Dict], current_month: str) -> Optional[float]:
94
  current_val = None
95
  prev_val = None
 
96
  for item in data:
97
  date = item['date']
98
  if date == current_month:
@@ -100,15 +122,24 @@ def get_yoy_change(data: List[Dict], current_month: str) -> Optional[float]:
100
  year_ago = str(int(date[:4]) - 1) + date[4:]
101
  if date == year_ago and date[:7] == current_month[:7]:
102
  prev_val = item['value']
 
103
  if current_val and prev_val:
104
  return ((current_val - prev_val) / prev_val) * 100
 
105
  return None
106
 
107
  # ================= АНАЛИЗ СЮРПРИЗА =================
108
  def calc_surprise(actual: float, consensus: float) -> Dict:
109
  if consensus == 0:
110
- return {"surprise_pct": 0, "level": "IN_LINE", "impact": 0, "direction": "NEUTRAL"}
 
 
 
 
 
 
111
  surprise_pct = ((actual - consensus) / abs(consensus)) * 100
 
112
  if abs(surprise_pct) > 100:
113
  level, impact = "EXTREME_SURPRISE", 30
114
  elif abs(surprise_pct) > 50:
@@ -119,33 +150,73 @@ def calc_surprise(actual: float, consensus: float) -> Dict:
119
  level, impact = "MINOR_SURPRISE", 5
120
  else:
121
  level, impact = "IN_LINE", 0
 
122
  direction = "POSITIVE" if surprise_pct > 0 else "NEGATIVE" if surprise_pct < 0 else "NEUTRAL"
123
- return {"surprise_pct": round(surprise_pct, 2), "level": level, "impact": impact, "direction": direction}
 
 
 
 
 
 
124
 
125
  # ================= FOMC SURPRISE (через новости) =================
126
  async def fetch_fomc_surprise() -> Dict:
127
  if not NEWSAPI_KEY:
128
- return {"indicator": "FOMC", "impact": 0, "direction": "NEUTRAL"}
 
 
 
 
 
129
  try:
130
  r = await http_client.get(
131
- f"https://newsapi.org/v2/everything?q=fomc+fed+rate+decision&pageSize=10&apiKey={NEWSAPI_KEY}")
 
132
  if r.status_code == 200:
133
  articles = r.json().get('articles', [])
134
- hawk = sum(1 for a in articles if any(w in (a.get('title','')+a.get('description','')).lower() for w in ['hawkish','raise','tighten','surprise hike']))
135
- dove = sum(1 for a in articles if any(w in (a.get('title','')+a.get('description','')).lower() for w in ['dovish','cut','ease','surprise cut']))
136
- if hawk > dove*2:
137
- return {"indicator": "FOMC", "impact": -15, "direction": "HAWKISH", "hawkish": hawk, "dovish": dove}
138
- elif dove > hawk*2:
139
- return {"indicator": "FOMC", "impact": 15, "direction": "DOVISH", "hawkish": hawk, "dovish": dove}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  except:
141
  pass
142
- return {"indicator": "FOMC", "impact": 0, "direction": "NEUTRAL"}
 
 
 
 
 
143
 
144
  # ================= ГЛАВНЫЙ АНАЛИЗ =================
145
  async def analyze_macro_surprises() -> Dict:
146
  today = datetime.now(timezone.utc)
147
  current_month_str = today.strftime("%Y-%m")
148
 
 
149
  cpi_data = await fetch_fred_series("CPIAUCSL", 13)
150
  core_cpi_data = await fetch_fred_series("CPILFESL", 13)
151
  unemp_data = await fetch_fred_series("UNRATE", 6)
@@ -158,51 +229,95 @@ async def analyze_macro_surprises() -> Dict:
158
 
159
  surprises = []
160
  total_score = 0.0
161
- weights = {"CPI_YOY": 0.30, "NFP": 0.25, "FOMC": 0.20, "ISM_MANUF": 0.10, "GDP_QOQ": 0.10, "RETAIL_SALES": 0.05}
 
 
 
 
 
 
 
162
 
 
163
  cpi_yoy = get_yoy_change(cpi_data, current_month_str)
164
  if cpi_yoy is not None:
165
  s = calc_surprise(cpi_yoy, CONSENSUS["CPI_YOY"])
166
  impact = s['impact'] * (1 if s['direction'] == 'NEGATIVE' else -0.5)
167
  total_score += impact * weights["CPI_YOY"]
168
- surprises.append({"indicator": "CPI_YOY", "actual": round(cpi_yoy, 2), "consensus": CONSENSUS["CPI_YOY"], "surprise": s})
 
 
 
 
 
169
 
 
170
  if nfp_data:
171
  nfp_actual = nfp_data[0]['value']
172
  s = calc_surprise(nfp_actual, CONSENSUS["NFP"])
173
  impact = s['impact'] * (1 if s['direction'] == 'POSITIVE' else -1)
174
  total_score += impact * weights["NFP"]
175
- surprises.append({"indicator": "NFP", "actual": int(nfp_actual), "consensus": CONSENSUS["NFP"], "surprise": s})
 
 
 
 
 
176
 
 
177
  if gdp_data:
178
  gdp_actual = gdp_data[0]['value']
179
  s = calc_surprise(gdp_actual, CONSENSUS["GDP_QOQ"])
180
  impact = s['impact'] * (1 if s['direction'] == 'POSITIVE' else -1)
181
  total_score += impact * weights["GDP_QOQ"]
182
- surprises.append({"indicator": "GDP_QOQ", "actual": round(gdp_actual, 2), "consensus": CONSENSUS["GDP_QOQ"], "surprise": s})
 
 
 
 
 
183
 
 
184
  if ism_data:
185
  ism_actual = ism_data[0]['value']
186
  s = calc_surprise(ism_actual, CONSENSUS["ISM_MANUF"])
187
  impact = s['impact'] * (1 if s['direction'] == 'POSITIVE' else -1)
188
  total_score += impact * weights["ISM_MANUF"]
189
- surprises.append({"indicator": "ISM_MANUF", "actual": round(ism_actual, 2), "consensus": CONSENSUS["ISM_MANUF"], "surprise": s})
 
 
 
 
 
190
 
 
191
  if retail_data:
192
  retail_actual = retail_data[0]['value']
193
  s = calc_surprise(retail_actual, CONSENSUS["RETAIL_SALES"])
194
  impact = s['impact'] * (1 if s['direction'] == 'POSITIVE' else -1)
195
  total_score += impact * weights["RETAIL_SALES"]
196
- surprises.append({"indicator": "RETAIL_SALES", "actual": round(retail_actual, 2), "consensus": CONSENSUS["RETAIL_SALES"], "surprise": s})
 
 
 
 
 
197
 
 
198
  if fomc_surprise['impact'] != 0:
199
  total_score += fomc_surprise['impact'] * weights["FOMC"]
200
- surprises.append({"indicator": "FOMC", "signal": fomc_surprise['direction'], "impact": fomc_surprise['impact']})
 
 
 
 
201
 
 
202
  total_score = max(-50, min(50, total_score))
203
  surprise_index = 50 + total_score
204
  surprise_index = max(0, min(100, surprise_index))
205
 
 
206
  if surprise_index > 65:
207
  regime, direction = "RISK_ON", "LONG"
208
  confidence = surprise_index / 100
@@ -213,6 +328,7 @@ async def analyze_macro_surprises() -> Dict:
213
  regime, direction = "NEUTRAL", "WAIT"
214
  confidence = 0.0
215
 
 
216
  SURPRISE_HISTORY.append({
217
  "timestamp": today.isoformat(),
218
  "surprise_index": round(surprise_index, 2),
@@ -232,15 +348,19 @@ async def analyze_macro_surprises() -> Dict:
232
 
233
  # ================= ОТПРАВКА В HUB =================
234
  async def send_signal_to_hub(symbol: str, direction: str, confidence: float):
 
235
  try:
236
- await http_client.post(f"{HUB_URL}/signal", json={
237
  "space": "space_29_macro_surprise",
238
  "symbol": symbol,
239
  "direction": direction,
240
  "confidence": confidence,
241
  "raw": json.dumps({"source": "space_29_macro_surprise"})
242
- })
243
- logger.info(f"📤 {symbol}: {direction} conf={confidence:.3f} отправлен в Hub")
 
 
 
244
  except Exception as e:
245
  logger.error(f"Ошибка отправки в Hub: {e}")
246
 
@@ -257,7 +377,13 @@ async def get_macro_surprise_signal() -> Dict[str, Any]:
257
  result = {
258
  "space": "space_29_macro_surprise",
259
  "timestamp": int(time.time()),
260
- "signals": {sym: {"direction": analysis['direction'], "confidence": analysis['confidence']} for sym in SYMBOLS},
 
 
 
 
 
 
261
  "surprise_analysis": analysis,
262
  "latency_ms": latency
263
  }
@@ -265,19 +391,45 @@ async def get_macro_surprise_signal() -> Dict[str, Any]:
265
  logger.info(f"📈 Macro Surprise: Index={analysis['surprise_index']:.1f} Regime={analysis['market_regime']}")
266
  return result
267
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
  # ================= FASTAPI =================
269
- app = FastAPI(title="Tomiris Space 29 v2.1 — Macro Surprise Engine (Hub)")
270
 
271
  @app.on_event("startup")
272
- async def startup(): pass
 
 
 
273
 
274
  @app.on_event("shutdown")
275
- async def shutdown(): await http_client.aclose()
 
276
 
277
  @app.get("/health")
278
  async def health():
279
- return {"status": "operational", "version": "2.1", "hub_connected": True,
280
- "indicators": list(CONSENSUS.keys()), "history_length": len(SURPRISE_HISTORY)}
 
 
 
 
 
 
281
 
282
  @app.get("/consilium")
283
  async def consilium():
@@ -295,8 +447,13 @@ async def fomc():
295
  async def history(limit: int = 50):
296
  return list(SURPRISE_HISTORY)[-limit:]
297
 
 
 
 
 
 
298
  if __name__ == "__main__":
299
  import uvicorn
300
  uvicorn.run(app, host="0.0.0.0", port=7860)
301
 
302
- print("🚀 SPACE 29 v2.1 — MACRO SURPRISE ENGINE (Hub-Connected) ЗАПУЩЕН!")
 
21
  print(f"✅ {pip_name} установлен!")
22
 
23
  # ============================================
24
+ # 👑 TOMIRIS SPACE 29 v2.2 — MACRO SURPRISE ENGINE (АВТО-ОТПРАВКА В HUB)
25
  # ============================================
26
  import os, time, json, logging, asyncio
27
  from typing import Dict, Any, List, Optional
 
41
  FRED_KEY = os.getenv("FRED_KEY", "faa11c8e2e4beee08c5b966e8b63a513")
42
  NEWSAPI_KEY = os.getenv("NEWSAPI_KEY", "948c7816beea47baa23b054592472d0e")
43
 
44
+ # Интервал авто-отправки (секунды)
45
+ AUTO_SEND_INTERVAL = int(os.getenv("AUTO_SEND_INTERVAL", "3600")) # раз в час — макро-данные медленные
46
+
47
  # Консенсус-прогнозы (обновлять ежемесячно)
48
  CONSENSUS = {
49
+ "CPI_YOY": 3.2,
50
+ "CORE_CPI_YOY": 3.5,
51
+ "UNEMPLOYMENT": 4.0,
52
+ "NFP": 180000,
53
+ "GDP_QOQ": 2.0,
54
+ "FED_RATE": 4.25,
55
+ "ISM_MANUF": 49.0,
56
+ "ISM_SERVICES": 52.0,
57
+ "RETAIL_SALES": 0.3,
58
+ "DURABLE_GOODS": 0.5
59
+ }
60
+
61
+ CACHE_TTL = {
62
+ "fred": 3600,
63
+ "news": 1800
64
  }
65
 
 
66
  HISTORY_FILE = "surprise_history.json"
67
 
68
  # ================= HTTP КЛИЕНТ =================
 
91
  cache_key = f"fred_{series_id}_{months}"
92
  if cache_key in cache_store and time.time() - cache_times.get(cache_key, 0) < CACHE_TTL["fred"]:
93
  return cache_store[cache_key]
94
+
95
  try:
96
  r = await http_client.get(
97
+ f"https://api.stlouisfed.org/fred/series/observations?series_id={series_id}&api_key={FRED_KEY}&file_type=json&sort_order=desc&limit={months}"
98
+ )
99
  if r.status_code == 200:
100
  data = r.json()
101
+ values = [
102
+ {'date': obs['date'], 'value': float(obs['value'])}
103
+ for obs in data.get('observations', [])
104
+ if obs['value'] != '.'
105
+ ]
106
  cache_store[cache_key] = values
107
  cache_times[cache_key] = time.time()
108
  return values
109
  except Exception as e:
110
  logger.warning(f"FRED {series_id}: {e}")
111
+
112
  return []
113
 
114
  def get_yoy_change(data: List[Dict], current_month: str) -> Optional[float]:
115
  current_val = None
116
  prev_val = None
117
+
118
  for item in data:
119
  date = item['date']
120
  if date == current_month:
 
122
  year_ago = str(int(date[:4]) - 1) + date[4:]
123
  if date == year_ago and date[:7] == current_month[:7]:
124
  prev_val = item['value']
125
+
126
  if current_val and prev_val:
127
  return ((current_val - prev_val) / prev_val) * 100
128
+
129
  return None
130
 
131
  # ================= АНАЛИЗ СЮРПРИЗА =================
132
  def calc_surprise(actual: float, consensus: float) -> Dict:
133
  if consensus == 0:
134
+ return {
135
+ "surprise_pct": 0,
136
+ "level": "IN_LINE",
137
+ "impact": 0,
138
+ "direction": "NEUTRAL"
139
+ }
140
+
141
  surprise_pct = ((actual - consensus) / abs(consensus)) * 100
142
+
143
  if abs(surprise_pct) > 100:
144
  level, impact = "EXTREME_SURPRISE", 30
145
  elif abs(surprise_pct) > 50:
 
150
  level, impact = "MINOR_SURPRISE", 5
151
  else:
152
  level, impact = "IN_LINE", 0
153
+
154
  direction = "POSITIVE" if surprise_pct > 0 else "NEGATIVE" if surprise_pct < 0 else "NEUTRAL"
155
+
156
+ return {
157
+ "surprise_pct": round(surprise_pct, 2),
158
+ "level": level,
159
+ "impact": impact,
160
+ "direction": direction
161
+ }
162
 
163
  # ================= FOMC SURPRISE (через новости) =================
164
  async def fetch_fomc_surprise() -> Dict:
165
  if not NEWSAPI_KEY:
166
+ return {
167
+ "indicator": "FOMC",
168
+ "impact": 0,
169
+ "direction": "NEUTRAL"
170
+ }
171
+
172
  try:
173
  r = await http_client.get(
174
+ f"https://newsapi.org/v2/everything?q=fomc+fed+rate+decision&pageSize=10&apiKey={NEWSAPI_KEY}"
175
+ )
176
  if r.status_code == 200:
177
  articles = r.json().get('articles', [])
178
+ hawk = sum(
179
+ 1 for a in articles
180
+ if any(w in (a.get('title', '') + a.get('description', '')).lower()
181
+ for w in ['hawkish', 'raise', 'tighten', 'surprise hike'])
182
+ )
183
+ dove = sum(
184
+ 1 for a in articles
185
+ if any(w in (a.get('title', '') + a.get('description', '')).lower()
186
+ for w in ['dovish', 'cut', 'ease', 'surprise cut'])
187
+ )
188
+
189
+ if hawk > dove * 2:
190
+ return {
191
+ "indicator": "FOMC",
192
+ "impact": -15,
193
+ "direction": "HAWKISH",
194
+ "hawkish": hawk,
195
+ "dovish": dove
196
+ }
197
+ elif dove > hawk * 2:
198
+ return {
199
+ "indicator": "FOMC",
200
+ "impact": 15,
201
+ "direction": "DOVISH",
202
+ "hawkish": hawk,
203
+ "dovish": dove
204
+ }
205
  except:
206
  pass
207
+
208
+ return {
209
+ "indicator": "FOMC",
210
+ "impact": 0,
211
+ "direction": "NEUTRAL"
212
+ }
213
 
214
  # ================= ГЛАВНЫЙ АНАЛИЗ =================
215
  async def analyze_macro_surprises() -> Dict:
216
  today = datetime.now(timezone.utc)
217
  current_month_str = today.strftime("%Y-%m")
218
 
219
+ # Загружаем все данные параллельно
220
  cpi_data = await fetch_fred_series("CPIAUCSL", 13)
221
  core_cpi_data = await fetch_fred_series("CPILFESL", 13)
222
  unemp_data = await fetch_fred_series("UNRATE", 6)
 
229
 
230
  surprises = []
231
  total_score = 0.0
232
+ weights = {
233
+ "CPI_YOY": 0.30,
234
+ "NFP": 0.25,
235
+ "FOMC": 0.20,
236
+ "ISM_MANUF": 0.10,
237
+ "GDP_QOQ": 0.10,
238
+ "RETAIL_SALES": 0.05
239
+ }
240
 
241
+ # CPI YoY
242
  cpi_yoy = get_yoy_change(cpi_data, current_month_str)
243
  if cpi_yoy is not None:
244
  s = calc_surprise(cpi_yoy, CONSENSUS["CPI_YOY"])
245
  impact = s['impact'] * (1 if s['direction'] == 'NEGATIVE' else -0.5)
246
  total_score += impact * weights["CPI_YOY"]
247
+ surprises.append({
248
+ "indicator": "CPI_YOY",
249
+ "actual": round(cpi_yoy, 2),
250
+ "consensus": CONSENSUS["CPI_YOY"],
251
+ "surprise": s
252
+ })
253
 
254
+ # NFP
255
  if nfp_data:
256
  nfp_actual = nfp_data[0]['value']
257
  s = calc_surprise(nfp_actual, CONSENSUS["NFP"])
258
  impact = s['impact'] * (1 if s['direction'] == 'POSITIVE' else -1)
259
  total_score += impact * weights["NFP"]
260
+ surprises.append({
261
+ "indicator": "NFP",
262
+ "actual": int(nfp_actual),
263
+ "consensus": CONSENSUS["NFP"],
264
+ "surprise": s
265
+ })
266
 
267
+ # GDP QoQ
268
  if gdp_data:
269
  gdp_actual = gdp_data[0]['value']
270
  s = calc_surprise(gdp_actual, CONSENSUS["GDP_QOQ"])
271
  impact = s['impact'] * (1 if s['direction'] == 'POSITIVE' else -1)
272
  total_score += impact * weights["GDP_QOQ"]
273
+ surprises.append({
274
+ "indicator": "GDP_QOQ",
275
+ "actual": round(gdp_actual, 2),
276
+ "consensus": CONSENSUS["GDP_QOQ"],
277
+ "surprise": s
278
+ })
279
 
280
+ # ISM Manufacturing
281
  if ism_data:
282
  ism_actual = ism_data[0]['value']
283
  s = calc_surprise(ism_actual, CONSENSUS["ISM_MANUF"])
284
  impact = s['impact'] * (1 if s['direction'] == 'POSITIVE' else -1)
285
  total_score += impact * weights["ISM_MANUF"]
286
+ surprises.append({
287
+ "indicator": "ISM_MANUF",
288
+ "actual": round(ism_actual, 2),
289
+ "consensus": CONSENSUS["ISM_MANUF"],
290
+ "surprise": s
291
+ })
292
 
293
+ # Retail Sales
294
  if retail_data:
295
  retail_actual = retail_data[0]['value']
296
  s = calc_surprise(retail_actual, CONSENSUS["RETAIL_SALES"])
297
  impact = s['impact'] * (1 if s['direction'] == 'POSITIVE' else -1)
298
  total_score += impact * weights["RETAIL_SALES"]
299
+ surprises.append({
300
+ "indicator": "RETAIL_SALES",
301
+ "actual": round(retail_actual, 2),
302
+ "consensus": CONSENSUS["RETAIL_SALES"],
303
+ "surprise": s
304
+ })
305
 
306
+ # FOMC Surprise
307
  if fomc_surprise['impact'] != 0:
308
  total_score += fomc_surprise['impact'] * weights["FOMC"]
309
+ surprises.append({
310
+ "indicator": "FOMC",
311
+ "signal": fomc_surprise['direction'],
312
+ "impact": fomc_surprise['impact']
313
+ })
314
 
315
+ # Нормализация
316
  total_score = max(-50, min(50, total_score))
317
  surprise_index = 50 + total_score
318
  surprise_index = max(0, min(100, surprise_index))
319
 
320
+ # Определение режима
321
  if surprise_index > 65:
322
  regime, direction = "RISK_ON", "LONG"
323
  confidence = surprise_index / 100
 
328
  regime, direction = "NEUTRAL", "WAIT"
329
  confidence = 0.0
330
 
331
+ # Сохраняем в историю
332
  SURPRISE_HISTORY.append({
333
  "timestamp": today.isoformat(),
334
  "surprise_index": round(surprise_index, 2),
 
348
 
349
  # ================= ОТПРАВКА В HUB =================
350
  async def send_signal_to_hub(symbol: str, direction: str, confidence: float):
351
+ """Отправка сигнала в Space 17 (Data Hub)."""
352
  try:
353
+ resp = await http_client.post(f"{HUB_URL}/signal", json={
354
  "space": "space_29_macro_surprise",
355
  "symbol": symbol,
356
  "direction": direction,
357
  "confidence": confidence,
358
  "raw": json.dumps({"source": "space_29_macro_surprise"})
359
+ }, timeout=10)
360
+ if resp.status_code == 200:
361
+ logger.info(f"📤 {symbol}: {direction} conf={confidence:.3f} отправлен в Hub")
362
+ else:
363
+ logger.warning(f"Hub вернул {resp.status_code}: {resp.text[:100]}")
364
  except Exception as e:
365
  logger.error(f"Ошибка отправки в Hub: {e}")
366
 
 
377
  result = {
378
  "space": "space_29_macro_surprise",
379
  "timestamp": int(time.time()),
380
+ "signals": {
381
+ sym: {
382
+ "direction": analysis['direction'],
383
+ "confidence": analysis['confidence']
384
+ }
385
+ for sym in SYMBOLS
386
+ },
387
  "surprise_analysis": analysis,
388
  "latency_ms": latency
389
  }
 
391
  logger.info(f"📈 Macro Surprise: Index={analysis['surprise_index']:.1f} Regime={analysis['market_regime']}")
392
  return result
393
 
394
+ # ================= АВТО-ОТПРАВКА ПО ТАЙМЕРУ =================
395
+ async def auto_send_loop():
396
+ """🔥 Фоновая задача: каждый час анализирует макро-сюрпризы и шлёт сигналы в Hub."""
397
+ logger.info(f"🔄 Авто-отправка Macro Surprise запущена (интервал {AUTO_SEND_INTERVAL}с)")
398
+ # Первый запуск через 30 секунд после старта
399
+ await asyncio.sleep(30)
400
+ while True:
401
+ try:
402
+ logger.info("📈 Macro Surprise авто-анализ...")
403
+ await get_macro_surprise_signal()
404
+ logger.info("✅ Macro Surprise авто-отправка завершена")
405
+ except Exception as e:
406
+ logger.error(f"Ошибка в авто-отправке: {e}")
407
+
408
+ await asyncio.sleep(AUTO_SEND_INTERVAL)
409
+
410
  # ================= FASTAPI =================
411
+ app = FastAPI(title="Tomiris Space 29 v2.2 — Macro Surprise Engine (Auto-Hub)")
412
 
413
  @app.on_event("startup")
414
+ async def startup():
415
+ # Запускаем ф��новую авто-отправку
416
+ asyncio.create_task(auto_send_loop())
417
+ logger.info("🚀 Space 29 v2.2 запущен с авто-отправкой в Hub")
418
 
419
  @app.on_event("shutdown")
420
+ async def shutdown():
421
+ await http_client.aclose()
422
 
423
  @app.get("/health")
424
  async def health():
425
+ return {
426
+ "status": "operational",
427
+ "version": "2.2",
428
+ "hub_url": HUB_URL,
429
+ "auto_send_interval": AUTO_SEND_INTERVAL,
430
+ "indicators": list(CONSENSUS.keys()),
431
+ "history_length": len(SURPRISE_HISTORY)
432
+ }
433
 
434
  @app.get("/consilium")
435
  async def consilium():
 
447
  async def history(limit: int = 50):
448
  return list(SURPRISE_HISTORY)[-limit:]
449
 
450
+ @app.get("/send_now")
451
+ async def send_now():
452
+ """Ручной триггер отправки."""
453
+ return await get_macro_surprise_signal()
454
+
455
  if __name__ == "__main__":
456
  import uvicorn
457
  uvicorn.run(app, host="0.0.0.0", port=7860)
458
 
459
+ print("🚀 SPACE 29 v2.2 — MACRO SURPRISE ENGINE (АВТО-ОТПРАВКА В HUB) ЗАПУЩЕН!")