tomirisg25 commited on
Commit
8f07b3a
·
verified ·
1 Parent(s): 17150f4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +223 -383
app.py CHANGED
@@ -1,12 +1,14 @@
1
  # ============================================
2
  # АВТО-УСТАНОВКА ПАКЕТОВ
3
  # ============================================
4
- import subprocess
5
- import sys
6
- import importlib
7
 
8
  REQUIRED_PACKAGES = {
9
  'numpy': 'numpy',
 
 
 
 
10
  'requests': 'requests'
11
  }
12
 
@@ -19,448 +21,286 @@ for module_name, pip_name in REQUIRED_PACKAGES.items():
19
  print(f"✅ {pip_name} установлен!")
20
 
21
  # ============================================
22
- # 👑 TOMIRIS SPACE 29 v1.0 — MACRO SURPRISE INDEX
23
  # ============================================
24
- # Оценивает, насколько макроданные отклоняются от консенсуса.
25
- # NFP, CPI, FOMC, PMI, GDP — сюрпризы двигают рынки.
26
- # Положительный сюрприз = риск-он, отрицательный = риск-офф.
27
- # ============================================
28
-
29
- from fastapi import FastAPI, Query
30
  from typing import Dict, Any, List, Optional
31
- import time
32
- import requests
33
- import threading
34
- import numpy as np
35
- from datetime import datetime, timedelta
36
  from collections import deque
37
- import warnings
38
- warnings.filterwarnings('ignore')
 
 
39
 
40
- # ================= КОНФИГУРАЦИЯ =================
41
- SYMBOLS: List[str] = ["XAU/USD", "ETH/USD", "SOL/USD"]
42
-
43
- SPACE_18_ARBITER: str = "https://tomiris-ai-name6-6.hf.space"
44
-
45
- # FRED API ключи
46
- FRED_KEYS: List[str] = [
47
- "faa11c8e2e4beee08c5b966e8b63a513",
48
- "3305a8458cb112b417183b1d6b87f2e1"
49
- ]
50
-
51
- # NewsAPI ключи
52
- NEWSAPI_KEYS: List[str] = [
53
- "948c7816beea47baa23b054592472d0e",
54
- "96199e06c4ac45e19236e2dfd879fd37"
55
- ]
56
-
57
- CACHE: Dict[str, Dict[str, Any]] = {}
58
- CACHE_TIMES: Dict[str, float] = {}
59
- FEATURES_STORE: Dict[str, Dict[str, Any]] = {}
60
- MT5_MAX_AGE_SEC: int = 300
61
- SURPRISE_HISTORY: deque = deque(maxlen=100)
62
-
63
- fred_counter: int = 0
64
- newsapi_counter: int = 0
65
- api_lock = threading.Lock()
66
-
67
- def get_next_fred_key() -> str:
68
- global fred_counter
69
- with api_lock:
70
- key = FRED_KEYS[fred_counter % len(FRED_KEYS)]
71
- fred_counter += 1
72
- return key
73
-
74
- def get_next_newsapi_key() -> str:
75
- global newsapi_counter
76
- with api_lock:
77
- key = NEWSAPI_KEYS[newsapi_counter % len(NEWSAPI_KEYS)]
78
- newsapi_counter += 1
79
- return key
80
-
81
- def send_to_arbiter(signal_data: Dict[str, Any]) -> None:
82
- try:
83
- requests.post(
84
- f"{SPACE_18_ARBITER}/log_signal",
85
- json={'space': 'space_29_macro_surprise', 'symbol': 'ALL', 'signal': signal_data.get('signal', {})},
86
- timeout=5
87
- )
88
- except:
89
- pass
90
 
91
- # ================= КОНСЕНСУС-ПРОГНОЗЫ (ОЖИДАНИЯ) =================
92
- # Аппроксимация рыночного консенсуса (обновляется ежемесячно)
93
- CONSENSUS: Dict[str, float] = {
94
- "CPI_YOY": 3.2, # Инфляция годовая
95
- "CORE_CPI_YOY": 3.5, # Базовая инфляция
96
- "UNEMPLOYMENT": 4.0, # Безработица
97
- "NFP": 180_000, # Non-Farm Payrolls
98
- "GDP_QOQ": 2.0, # ВВП квартальный
99
- "FED_RATE": 4.25, # Ставка ФРС
100
- "ISM_MANUF": 49.0, # ISM Manufacturing
101
- "ISM_SERVICES": 52.0, # ISM Services
102
- "RETAIL_SALES": 0.3, # Розничные продажи
103
- "DURABLE_GOODS": 0.5, # Заказы длительного пользования
104
  }
105
 
106
- # ================= ЗАГРУЗКА FRED =================
107
- def fetch_fred_series(series_id: str, days: int = 12) -> List[Dict[str, Any]]:
108
- cache_key = f"fred_{series_id}_{days}"
109
- if cache_key in CACHE and time.time() - CACHE_TIMES.get(cache_key, 0) < 3600:
110
- return CACHE[cache_key]
111
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  try:
113
- key = get_next_fred_key()
114
- url = f"https://api.stlouisfed.org/fred/series/observations?series_id={series_id}&api_key={key}&file_type=json&sort_order=desc&limit={days}"
115
- r = requests.get(url, timeout=10)
116
  if r.status_code == 200:
117
  data = r.json()
118
- values = [
119
- {'date': obs['date'], 'value': float(obs['value'])}
120
- for obs in data.get('observations', []) if obs['value'] != '.'
121
- ]
122
- CACHE[cache_key] = values
123
- CACHE_TIMES[cache_key] = time.time()
124
  return values
125
- except:
126
- pass
127
  return []
128
 
129
- # ================= РАСЧЁТ СЮРПРИЗА =================
130
- def calculate_surprise(actual: float, consensus: float) -> Dict[str, Any]:
131
- """Вычисляет, насколько факт отклонился от консенсуса."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  if consensus == 0:
133
- return {"surprise_pct": 0, "level": "NEUTRAL", "impact": 0}
134
-
135
  surprise_pct = ((actual - consensus) / abs(consensus)) * 100
136
-
137
  if abs(surprise_pct) > 100:
138
- level = "EXTREME_SURPRISE"
139
- impact = 30
140
  elif abs(surprise_pct) > 50:
141
- level = "MAJOR_SURPRISE"
142
- impact = 20
143
  elif abs(surprise_pct) > 20:
144
- level = "MODERATE_SURPRISE"
145
- impact = 10
146
  elif abs(surprise_pct) > 5:
147
- level = "MINOR_SURPRISE"
148
- impact = 5
149
  else:
150
- level = "IN_LINE"
151
- impact = 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
 
153
- return {
154
- "surprise_pct": round(surprise_pct, 2),
155
- "level": level,
156
- "impact": impact,
157
- "direction": "POSITIVE" if surprise_pct > 0 else "NEGATIVE" if surprise_pct < 0 else "NEUTRAL"
158
- }
 
 
 
 
 
 
 
 
 
159
 
160
- # ================= ОЦЕНКА МАКРО-СЮРПРИЗОВ =================
161
- def analyze_macro_surprises() -> Dict[str, Any]:
162
  surprises = []
163
- total_surprise_score = 0.0
164
- positive_impact = 0.0
165
- negative_impact = 0.0
166
-
167
- # CPI (CPIAUCSL)
168
- cpi_data = fetch_fred_series("CPIAUCSL", 12)
169
- if len(cpi_data) >= 2:
170
- cpi_current = cpi_data[0]['value']
171
- cpi_prev = cpi_data[-1]['value']
172
- cpi_yoy = ((cpi_current - cpi_prev) / cpi_prev) * 100
173
- surprise = calculate_surprise(cpi_yoy, CONSENSUS.get("CPI_YOY", 3.2))
174
-
175
- if surprise['direction'] == 'POSITIVE':
176
- # Инфляция выше ожиданий = негатив для риска
177
- negative_impact += surprise['impact']
178
- total_surprise_score -= surprise['impact']
179
- signal = "BEARISH"
180
- else:
181
- positive_impact += surprise['impact']
182
- total_surprise_score += surprise['impact']
183
- signal = "BULLISH"
184
-
185
- surprises.append({
186
- "indicator": "CPI_YOY",
187
- "actual": round(cpi_yoy, 2),
188
- "consensus": CONSENSUS.get("CPI_YOY", 3.2),
189
- "surprise": surprise,
190
- "market_signal": signal
191
- })
192
-
193
- # Unemployment (UNRATE)
194
- unemp_data = fetch_fred_series("UNRATE", 6)
195
- if unemp_data:
196
- unemp_current = unemp_data[0]['value']
197
- surprise = calculate_surprise(unemp_current, CONSENSUS.get("UNEMPLOYMENT", 4.0))
198
-
199
- if surprise['direction'] == 'POSITIVE':
200
- # Безработица выше ожиданий = негатив
201
- negative_impact += surprise['impact']
202
- total_surprise_score -= surprise['impact']
203
- signal = "BEARISH"
204
- else:
205
- # Безработица ниже = позитив
206
- positive_impact += surprise['impact']
207
- total_surprise_score += surprise['impact']
208
- signal = "BULLISH"
209
-
210
- surprises.append({
211
- "indicator": "UNEMPLOYMENT",
212
- "actual": round(unemp_current, 2),
213
- "consensus": CONSENSUS.get("UNEMPLOYMENT", 4.0),
214
- "surprise": surprise,
215
- "market_signal": signal
216
- })
217
-
218
- # FOMC — через новости
219
- fomc_surprise = fetch_fomc_surprise()
220
- if fomc_surprise.get('impact', 0) != 0:
221
- surprises.append(fomc_surprise)
222
- total_surprise_score += fomc_surprise.get('impact', 0)
223
- if fomc_surprise.get('direction') == 'HAWKISH':
224
- negative_impact += abs(fomc_surprise.get('impact', 0))
225
- else:
226
- positive_impact += abs(fomc_surprise.get('impact', 0))
227
-
228
- # Нормализация
229
- total_surprise_score = max(-50, min(50, total_surprise_score))
230
- surprise_index = 50 + total_surprise_score
231
  surprise_index = max(0, min(100, surprise_index))
232
 
233
  if surprise_index > 65:
234
- market_regime = "RISK_ON"
235
- direction = "LONG"
236
  confidence = surprise_index / 100
237
  elif surprise_index < 35:
238
- market_regime = "RISK_OFF"
239
- direction = "SHORT"
240
  confidence = (100 - surprise_index) / 100
241
  else:
242
- market_regime = "NEUTRAL"
243
- direction = "WAIT"
244
  confidence = 0.0
245
 
 
 
 
 
 
 
 
 
 
246
  return {
247
  "surprise_index": round(surprise_index, 2),
248
- "market_regime": market_regime,
249
  "direction": direction,
250
  "confidence": round(confidence, 4),
251
- "total_surprise_score": round(total_surprise_score, 2),
252
- "positive_impact": round(positive_impact, 2),
253
- "negative_impact": round(negative_impact, 2),
254
- "surprises": surprises
255
  }
256
 
257
- # ================= FOMC СЮРПРИЗ =================
258
- def fetch_fomc_surprise() -> Dict[str, Any]:
259
- """Оценивает сюрприз от решений ФРС через новости."""
260
- cache_key = "fomc_surprise"
261
- if cache_key in CACHE and time.time() - CACHE_TIMES.get(cache_key, 0) < 3600:
262
- return CACHE[cache_key]
263
-
264
- try:
265
- key = get_next_newsapi_key()
266
- url = f"https://newsapi.org/v2/everything?q=fomc+federal+reserve+surprise+rate&pageSize=10&apiKey={key}"
267
- r = requests.get(url, timeout=10)
268
- if r.status_code == 200:
269
- articles = r.json().get('articles', [])
270
-
271
- hawkish_count = 0
272
- dovish_count = 0
273
-
274
- for a in articles:
275
- title = a.get('title', '').lower()
276
- desc = a.get('description', '').lower()
277
- text = title + ' ' + desc
278
-
279
- if any(kw in text for kw in ['hawkish', 'raise', 'tighten', 'surprise hike']):
280
- hawkish_count += 1
281
- if any(kw in text for kw in ['dovish', 'cut', 'ease', 'surprise cut']):
282
- dovish_count += 1
283
-
284
- if hawkish_count > dovish_count * 2:
285
- signal = "HAWKISH_SURPRISE"
286
- impact = -15
287
- elif dovish_count > hawkish_count * 2:
288
- signal = "DOVISH_SURPRISE"
289
- impact = 15
290
- else:
291
- signal = "NO_SURPRISE"
292
- impact = 0
293
-
294
- result = {
295
- "indicator": "FOMC",
296
- "signal": signal,
297
- "impact": impact,
298
- "hawkish_articles": hawkish_count,
299
- "dovish_articles": dovish_count,
300
- "direction": "HAWKISH" if impact < 0 else "DOVISH" if impact > 0 else "NEUTRAL"
301
- }
302
-
303
- CACHE[cache_key] = result
304
- CACHE_TIMES[cache_key] = time.time()
305
- return result
306
- except:
307
- pass
308
-
309
- return {"indicator": "FOMC", "impact": 0, "direction": "NEUTRAL"}
310
-
311
- # ================= АНАЛИЗ ИЗ MT5 =================
312
- def analyze_from_mt5(mt5_features: Dict[str, Any]) -> Optional[Dict[str, Any]]:
313
- try:
314
- score = 50.0
315
- now = datetime.utcnow()
316
- hour = now.hour
317
- weekday = now.weekday()
318
-
319
- # В часы выхода важных данных (12-14 UTC) — ожидаем сюрпризов
320
- if hour in [12, 13, 14]:
321
- if weekday in [2, 3, 4]: # Ср-Пт — дни NFP/CPI
322
- score -= 10
323
-
324
- rsi = mt5_features.get('H1_rsi', 50)
325
- atr = mt5_features.get('H1_atr_pct', 1)
326
-
327
- if isinstance(rsi, (int, float)) and isinstance(atr, (int, float)):
328
- if atr > 2 and rsi > 70:
329
- score += 15
330
- elif atr > 2 and rsi < 30:
331
- score -= 15
332
-
333
- score = max(0, min(100, score))
334
-
335
- if score > 60:
336
- direction, confidence = "LONG", score / 100
337
- elif score < 40:
338
- direction, confidence = "SHORT", (100 - score) / 100
339
- else:
340
- direction, confidence = "WAIT", 0.0
341
-
342
- return {
343
- "direction": direction,
344
- "confidence": round(confidence, 4),
345
- "surprise_index": score,
346
- "source": "MT5"
347
- }
348
- except:
349
- return None
350
-
351
  # ================= ГЛАВНЫЙ СИГНАЛ =================
352
- def get_macro_surprise_signal() -> Dict[str, Any]:
353
  start = time.time()
354
-
355
- # Проверка MT5
356
- for symbol in SYMBOLS:
357
- if symbol in FEATURES_STORE:
358
- fs = FEATURES_STORE[symbol]
359
- age = time.time() - fs.get("timestamp", 0)
360
- if age < MT5_MAX_AGE_SEC:
361
- mt5_features = fs.get("features", {})
362
- if mt5_features:
363
- mt5_result = analyze_from_mt5(mt5_features)
364
- if mt5_result and mt5_result["confidence"] > 0.3:
365
- result = {
366
- "space": "space_29_macro_surprise",
367
- "timestamp": int(time.time()),
368
- "signals": {
369
- s: {"direction": mt5_result["direction"], "confidence": mt5_result["confidence"]}
370
- for s in SYMBOLS
371
- },
372
- "surprise_index": mt5_result["surprise_index"],
373
- "data_source": "MT5",
374
- "meta": {"latency_ms": int((time.time() - start) * 1000)}
375
- }
376
- send_to_arbiter(result)
377
- print(f"📈 MACRO SURPRISE: {mt5_result['direction']} | Index={mt5_result['surprise_index']} (MT5)")
378
- return result
379
-
380
- # Fallback
381
- analysis = analyze_macro_surprises()
382
  latency = int((time.time() - start) * 1000)
383
 
384
  result = {
385
  "space": "space_29_macro_surprise",
386
  "timestamp": int(time.time()),
387
- "signals": {
388
- s: {
389
- "direction": analysis['direction'],
390
- "confidence": analysis['confidence']
391
- }
392
- for s in SYMBOLS
393
- },
394
- "surprise_analysis": {
395
- "surprise_index": analysis['surprise_index'],
396
- "market_regime": analysis['market_regime'],
397
- "surprises": analysis['surprises'],
398
- "positive_impact": analysis['positive_impact'],
399
- "negative_impact": analysis['negative_impact']
400
- },
401
- "data_source": "FRED+NEWS",
402
- "meta": {"latency_ms": latency}
403
  }
404
 
405
- send_to_arbiter(result)
406
- print(f"📈 MACRO SURPRISE v1: Index={analysis['surprise_index']:.0f} | Regime={analysis['market_regime']} | Surprises={len(analysis['surprises'])}")
 
 
 
 
 
 
 
 
 
 
407
  return result
408
 
409
- # ================= KEEP-ALIVE =================
410
- def keep_alive():
411
- while True:
412
- time.sleep(840)
413
- try:
414
- requests.get("http://localhost:7860/health", timeout=5)
415
- except:
416
- pass
417
 
418
- threading.Thread(target=keep_alive, daemon=True).start()
 
419
 
420
- # ================= FASTAPI =================
421
- app = FastAPI(title="TOMIRIS SPACE 29 v1.0 — MACRO SURPRISE INDEX")
422
 
423
  @app.get("/health")
424
- @app.head("/health")
425
  async def health():
426
- return {
427
- "space": "Space 29 - Macro Surprise Index v1.0",
428
- "status": "operational",
429
- "symbols": SYMBOLS,
430
- "indicators": list(CONSENSUS.keys()),
431
- "features": ["CPI Surprise", "NFP Surprise", "FOMC Surprise", "Citigroup Surprise Index"]
432
- }
433
 
434
  @app.get("/consilium")
435
  async def consilium():
436
- return get_macro_surprise_signal()
437
 
438
  @app.get("/surprise_index")
439
  async def surprise_index():
440
- return analyze_macro_surprises()
441
 
442
  @app.get("/fomc")
443
  async def fomc():
444
- return fetch_fomc_surprise()
445
-
446
- @app.get("/indicator/{name}")
447
- async def indicator(name: str):
448
- consensus = CONSENSUS.get(name.upper())
449
- if consensus is None:
450
- return {"error": f"Unknown: {name}", "available": list(CONSENSUS.keys())}
451
- return {"indicator": name, "consensus": consensus}
452
-
453
- @app.post("/features")
454
- async def receive_features(data: Dict[str, Any]):
455
- symbol = data.get("symbol", "XAU/USD")
456
- FEATURES_STORE[symbol] = {
457
- "features": data.get("features", {}),
458
- "price": data.get("price", 0.0),
459
- "timestamp": time.time()
460
- }
461
- print(f"📥 MT5 {symbol}: {len(data.get('features', {}))} признаков")
462
- return {"status": "ok"}
463
 
464
- print(f"🚀 SPACE 29 v1.0 — MACRO SURPRISE INDEX ЗАПУЩЕН!")
465
- print(f"📈 Анализ: CPI | NFP | FOMC | GDP | ISM | Сравнение с консенсусом")
466
- print(f"✅ Готов к бою!")
 
1
  # ============================================
2
  # АВТО-УСТАНОВКА ПАКЕТОВ
3
  # ============================================
4
+ import subprocess, sys, importlib
 
 
5
 
6
  REQUIRED_PACKAGES = {
7
  'numpy': 'numpy',
8
+ 'pandas': 'pandas',
9
+ 'httpx': 'httpx',
10
+ 'fastapi': 'fastapi',
11
+ 'uvicorn': 'uvicorn',
12
  'requests': 'requests'
13
  }
14
 
 
21
  print(f"✅ {pip_name} установлен!")
22
 
23
  # ============================================
24
+ # 👑 TOMIRIS SPACE 29 v2.0 — MACRO SURPRISE ENGINE (Async, Real FRED, Fixed CPI)
25
  # ============================================
26
+ import os, time, json, logging, asyncio
 
 
 
 
 
27
  from typing import Dict, Any, List, Optional
28
+ from datetime import datetime, timezone, timedelta
 
 
 
 
29
  from collections import deque
30
+ import numpy as np
31
+ import pandas as pd
32
+ import httpx
33
+ from fastapi import FastAPI, Query
34
 
35
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
36
+ logger = logging.getLogger("Space29_MacroSurprise")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
+ # ================= КОНФИГУРАЦИЯ =================
39
+ SYMBOLS = ["XAU/USD", "ETH/USD", "SOL/USD"]
40
+ ARBITER_URL = os.getenv("SPACE18_URL", "https://tomiris-ai-name6-6.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 КЛИЕНТ =================
55
+ http_client = httpx.AsyncClient(timeout=20.0)
56
 
57
+ # ================= ГЛОБАЛЬНЫЙ КЭШ =================
58
+ cache_store = {}
59
+ cache_times = {}
60
+
61
+ # История сюрпризов
62
+ if os.path.exists(HISTORY_FILE):
63
+ try:
64
+ with open(HISTORY_FILE) as f:
65
+ SURPRISE_HISTORY = deque(json.load(f), maxlen=200)
66
+ except:
67
+ SURPRISE_HISTORY = deque(maxlen=200)
68
+ else:
69
+ SURPRISE_HISTORY = deque(maxlen=200)
70
+
71
+ def save_history():
72
+ with open(HISTORY_FILE, 'w') as f:
73
+ json.dump(list(SURPRISE_HISTORY), f)
74
+
75
+ # ================= ЗАГРУЗКА ДАННЫХ FRED =================
76
+ 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
+ """Считает YoY изменение: текущее значение vs значение 12 месяцев назад (тот же месяц)."""
95
+ # Ищем записи с нужным месяцем
96
+ current_val = None
97
+ prev_val = None
98
+ for item in data:
99
+ date = item['date']
100
+ if date == current_month:
101
+ current_val = item['value']
102
+ # Тот же месяц, год назад (YYYY-1)
103
+ year_ago = str(int(date[:4]) - 1) + date[4:]
104
+ if date == year_ago and date[:7] == current_month[:7]:
105
+ prev_val = item['value']
106
+ if current_val and prev_val:
107
+ return ((current_val - prev_val) / prev_val) * 100
108
+ return None
109
+
110
+ # ================= АНАЛИЗ СЮРПРИЗА =================
111
+ def calc_surprise(actual: float, consensus: float) -> Dict:
112
  if consensus == 0:
113
+ return {"surprise_pct": 0, "level": "IN_LINE", "impact": 0, "direction": "NEUTRAL"}
 
114
  surprise_pct = ((actual - consensus) / abs(consensus)) * 100
 
115
  if abs(surprise_pct) > 100:
116
+ level, impact = "EXTREME_SURPRISE", 30
 
117
  elif abs(surprise_pct) > 50:
118
+ level, impact = "MAJOR_SURPRISE", 20
 
119
  elif abs(surprise_pct) > 20:
120
+ level, impact = "MODERATE_SURPRISE", 10
 
121
  elif abs(surprise_pct) > 5:
122
+ level, impact = "MINOR_SURPRISE", 5
 
123
  else:
124
+ level, impact = "IN_LINE", 0
125
+ direction = "POSITIVE" if surprise_pct > 0 else "NEGATIVE" if surprise_pct < 0 else "NEUTRAL"
126
+ return {"surprise_pct": round(surprise_pct, 2), "level": level, "impact": impact, "direction": direction}
127
+
128
+ # ================= FOMC SURPRISE (через новости) =================
129
+ async def fetch_fomc_surprise() -> Dict:
130
+ if not NEWSAPI_KEY:
131
+ return {"indicator": "FOMC", "impact": 0, "direction": "NEUTRAL"}
132
+ try:
133
+ r = await http_client.get(
134
+ f"https://newsapi.org/v2/everything?q=fomc+fed+rate+decision&pageSize=10&apiKey={NEWSAPI_KEY}")
135
+ if r.status_code == 200:
136
+ articles = r.json().get('articles', [])
137
+ 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']))
138
+ 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']))
139
+ if hawk > dove*2:
140
+ return {"indicator": "FOMC", "impact": -15, "direction": "HAWKISH", "hawkish": hawk, "dovish": dove}
141
+ elif dove > hawk*2:
142
+ return {"indicator": "FOMC", "impact": 15, "direction": "DOVISH", "hawkish": hawk, "dovish": dove}
143
+ except:
144
+ pass
145
+ return {"indicator": "FOMC", "impact": 0, "direction": "NEUTRAL"}
146
 
147
+ # ================= ГЛАВНЫЙ АНАЛИЗ =================
148
+ async def analyze_macro_surprises() -> Dict:
149
+ today = datetime.now(timezone.utc)
150
+ current_month_str = today.strftime("%Y-%m")
151
+
152
+ # Загружаем FRED параллельно
153
+ cpi_data = await fetch_fred_series("CPIAUCSL", 13)
154
+ core_cpi_data = await fetch_fred_series("CPILFESL", 13)
155
+ unemp_data = await fetch_fred_series("UNRATE", 6)
156
+ nfp_data = await fetch_fred_series("PAYEMS", 3) # Non-Farm Payrolls
157
+ gdp_data = await fetch_fred_series("GDP", 3) # GDP (квартальный, но берём последний)
158
+ ism_data = await fetch_fred_series("NAPM", 3) # ISM Manufacturing
159
+ retail_data = await fetch_fred_series("RSAFS", 3) # Retail Sales
160
+ durable_data = await fetch_fred_series("DGORDER", 3) # Durable Goods
161
+ fomc_surprise = await fetch_fomc_surprise()
162
 
 
 
163
  surprises = []
164
+ total_score = 0.0
165
+ weights = {"CPI_YOY": 0.30, "NFP": 0.25, "FOMC": 0.20, "ISM_MANUF": 0.10, "GDP_QOQ": 0.10, "RETAIL_SALES": 0.05}
166
+
167
+ # CPI YoY (корректный)
168
+ cpi_yoy = get_yoy_change(cpi_data, current_month_str)
169
+ if cpi_yoy is not None:
170
+ s = calc_surprise(cpi_yoy, CONSENSUS["CPI_YOY"])
171
+ impact = s['impact'] * (1 if s['direction'] == 'NEGATIVE' else -0.5) # высокая инфляция = риск-офф
172
+ total_score += impact * weights["CPI_YOY"]
173
+ surprises.append({"indicator": "CPI_YOY", "actual": round(cpi_yoy, 2), "consensus": CONSENSUS["CPI_YOY"], "surprise": s})
174
+
175
+ # NFP (последний месяц)
176
+ if nfp_data:
177
+ nfp_actual = nfp_data[0]['value'] # абсолютное значение в тысячах
178
+ s = calc_surprise(nfp_actual, CONSENSUS["NFP"])
179
+ impact = s['impact'] * (1 if s['direction'] == 'POSITIVE' else -1) # высокий NFP = риск-он
180
+ total_score += impact * weights["NFP"]
181
+ surprises.append({"indicator": "NFP", "actual": int(nfp_actual), "consensus": CONSENSUS["NFP"], "surprise": s})
182
+
183
+ # GDP (квартальный, берём последний)
184
+ if gdp_data:
185
+ gdp_actual = gdp_data[0]['value'] # квартальное изменение в %
186
+ s = calc_surprise(gdp_actual, CONSENSUS["GDP_QOQ"])
187
+ impact = s['impact'] * (1 if s['direction'] == 'POSITIVE' else -1)
188
+ total_score += impact * weights["GDP_QOQ"]
189
+ surprises.append({"indicator": "GDP_QOQ", "actual": round(gdp_actual, 2), "consensus": CONSENSUS["GDP_QOQ"], "surprise": s})
190
+
191
+ # ISM Manufacturing
192
+ if ism_data:
193
+ ism_actual = ism_data[0]['value']
194
+ s = calc_surprise(ism_actual, CONSENSUS["ISM_MANUF"])
195
+ impact = s['impact'] * (1 if s['direction'] == 'POSITIVE' else -1)
196
+ total_score += impact * weights["ISM_MANUF"]
197
+ surprises.append({"indicator": "ISM_MANUF", "actual": round(ism_actual, 2), "consensus": CONSENSUS["ISM_MANUF"], "surprise": s})
198
+
199
+ # Retail Sales
200
+ if retail_data:
201
+ retail_actual = retail_data[0]['value']
202
+ s = calc_surprise(retail_actual, CONSENSUS["RETAIL_SALES"])
203
+ impact = s['impact'] * (1 if s['direction'] == 'POSITIVE' else -1)
204
+ total_score += impact * weights["RETAIL_SALES"]
205
+ surprises.append({"indicator": "RETAIL_SALES", "actual": round(retail_actual, 2), "consensus": CONSENSUS["RETAIL_SALES"], "surprise": s})
206
+
207
+ # FOMC
208
+ if fomc_surprise['impact'] != 0:
209
+ total_score += fomc_surprise['impact'] * weights["FOMC"]
210
+ surprises.append({"indicator": "FOMC", "signal": fomc_surprise['direction'], "impact": fomc_surprise['impact']})
211
+
212
+ total_score = max(-50, min(50, total_score))
213
+ surprise_index = 50 + total_score
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  surprise_index = max(0, min(100, surprise_index))
215
 
216
  if surprise_index > 65:
217
+ regime, direction = "RISK_ON", "LONG"
 
218
  confidence = surprise_index / 100
219
  elif surprise_index < 35:
220
+ regime, direction = "RISK_OFF", "SHORT"
 
221
  confidence = (100 - surprise_index) / 100
222
  else:
223
+ regime, direction = "NEUTRAL", "WAIT"
 
224
  confidence = 0.0
225
 
226
+ # Сохраняем историю
227
+ SURPRISE_HISTORY.append({
228
+ "timestamp": today.isoformat(),
229
+ "surprise_index": round(surprise_index, 2),
230
+ "regime": regime,
231
+ "surprises": surprises
232
+ })
233
+ save_history()
234
+
235
  return {
236
  "surprise_index": round(surprise_index, 2),
237
+ "market_regime": regime,
238
  "direction": direction,
239
  "confidence": round(confidence, 4),
240
+ "surprises": surprises,
241
+ "total_score": round(total_score, 2)
 
 
242
  }
243
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
  # ================= ГЛАВНЫЙ СИГНАЛ =================
245
+ async def get_macro_surprise_signal() -> Dict[str, Any]:
246
  start = time.time()
247
+ analysis = await analyze_macro_surprises()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
  latency = int((time.time() - start) * 1000)
249
 
250
  result = {
251
  "space": "space_29_macro_surprise",
252
  "timestamp": int(time.time()),
253
+ "signals": {sym: {"direction": analysis['direction'], "confidence": analysis['confidence']} for sym in SYMBOLS},
254
+ "surprise_analysis": analysis,
255
+ "latency_ms": latency
 
 
 
 
 
 
 
 
 
 
 
 
 
256
  }
257
 
258
+ # Отправка в Arbiter
259
+ try:
260
+ await http_client.post(f"{ARBITER_URL}/log_signal", json={
261
+ "space": "space_29_macro_surprise",
262
+ "symbol": "XAU/USD",
263
+ "signal": result["signals"]["XAU/USD"],
264
+ "surprise_details": analysis
265
+ })
266
+ except:
267
+ pass
268
+
269
+ logger.info(f"📈 Macro Surprise: Index={analysis['surprise_index']:.1f} Regime={analysis['market_regime']}")
270
  return result
271
 
272
+ # ================= FASTAPI =================
273
+ app = FastAPI(title="Tomiris Space 29 v2.0 — Macro Surprise Engine")
 
 
 
 
 
 
274
 
275
+ @app.on_event("startup")
276
+ async def startup(): pass
277
 
278
+ @app.on_event("shutdown")
279
+ async def shutdown(): await http_client.aclose()
280
 
281
  @app.get("/health")
 
282
  async def health():
283
+ return {"status": "operational", "version": "2.0", "async": True, "no_mt5": True,
284
+ "indicators": list(CONSENSUS.keys()), "history_length": len(SURPRISE_HISTORY)}
 
 
 
 
 
285
 
286
  @app.get("/consilium")
287
  async def consilium():
288
+ return await get_macro_surprise_signal()
289
 
290
  @app.get("/surprise_index")
291
  async def surprise_index():
292
+ return await analyze_macro_surprises()
293
 
294
  @app.get("/fomc")
295
  async def fomc():
296
+ return await fetch_fomc_surprise()
297
+
298
+ @app.get("/history")
299
+ async def history(limit: int = 50):
300
+ return list(SURPRISE_HISTORY)[-limit:]
301
+
302
+ if __name__ == "__main__":
303
+ import uvicorn
304
+ uvicorn.run(app, host="0.0.0.0", port=7860)
 
 
 
 
 
 
 
 
 
 
305
 
306
+ print("🚀 SPACE 29 v2.0 — MACRO SURPRISE ENGINE (REAL FRED, ASYNC) ЗАПУЩЕН!")