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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +328 -308
app.py CHANGED
@@ -7,7 +7,6 @@ import importlib
7
 
8
  REQUIRED_PACKAGES = {
9
  'numpy': 'numpy',
10
- 'pandas': 'pandas',
11
  'requests': 'requests'
12
  }
13
 
@@ -20,21 +19,20 @@ for module_name, pip_name in REQUIRED_PACKAGES.items():
20
  print(f"✅ {pip_name} установлен!")
21
 
22
  # ============================================
23
- # 👑 TOMIRIS SPACE 28 v1.0 — MARKET REGIME & BUBBLE DETECTOR
24
  # ============================================
25
- # Определяет режим рынка (TREND/RANGE/VOLATILE/BUBBLE).
26
- # Euphoria Index, NVT Ratio, RSI экстремумы, пузыри.
27
- # Имеет право ВЕТО — при пузыре блокирует BUY.
28
  # ============================================
29
 
30
  from fastapi import FastAPI, Query
31
- from typing import Dict, Any, List, Optional, Tuple
32
  import time
33
  import requests
34
  import threading
35
  import numpy as np
36
- import pandas as pd
37
- from datetime import datetime
38
  from collections import deque
39
  import warnings
40
  warnings.filterwarnings('ignore')
@@ -44,326 +42,368 @@ SYMBOLS: List[str] = ["XAU/USD", "ETH/USD", "SOL/USD"]
44
 
45
  SPACE_18_ARBITER: str = "https://tomiris-ai-name6-6.hf.space"
46
 
47
- TWELVE_KEYS: List[str] = [
48
- "e3740c072fda4fe8b8539d40b07e445e",
49
- "58e67e0008e24161ac9b1671b7c2d2d0"
 
50
  ]
51
 
52
- CACHE: Dict[str, Any] = {}
 
 
 
 
 
 
53
  CACHE_TIMES: Dict[str, float] = {}
54
  FEATURES_STORE: Dict[str, Dict[str, Any]] = {}
55
  MT5_MAX_AGE_SEC: int = 300
56
- BUBBLE_HISTORY: deque = deque(maxlen=200)
57
 
58
- twelve_counter: int = 0
 
59
  api_lock = threading.Lock()
60
 
61
- def get_next_key() -> str:
62
- global twelve_counter
 
 
 
 
 
 
 
63
  with api_lock:
64
- key = TWELVE_KEYS[twelve_counter % len(TWELVE_KEYS)]
65
- twelve_counter += 1
66
  return key
67
 
68
  def send_to_arbiter(signal_data: Dict[str, Any]) -> None:
69
  try:
70
  requests.post(
71
  f"{SPACE_18_ARBITER}/log_signal",
72
- json={'space': 'space_28_regime', 'symbol': 'ALL', 'signal': signal_data.get('signal', {})},
73
  timeout=5
74
  )
75
  except:
76
  pass
77
 
78
- # ================= ЗАГРУЗКА ДАННЫХ =================
79
- def fetch_historical(symbol: str, tf: str = "1h", count: int = 200) -> Optional[pd.DataFrame]:
80
- cache_key = f"hist_{symbol}_{tf}_{count}"
81
- if cache_key in CACHE and time.time() - CACHE_TIMES.get(cache_key, 0) < 300:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  return CACHE[cache_key]
83
 
84
  try:
85
- key = get_next_key()
86
- twelve_symbol = symbol.replace("/", "")
87
- url = f"https://api.twelvedata.com/time_series?symbol={twelve_symbol}&interval={tf}&outputsize={count}&apikey={key}"
88
  r = requests.get(url, timeout=10)
89
  if r.status_code == 200:
90
  data = r.json()
91
- if 'values' in data:
92
- df = pd.DataFrame(data['values']).iloc[::-1]
93
- df['close'] = pd.to_numeric(df['close'])
94
- df['high'] = pd.to_numeric(df['high'])
95
- df['low'] = pd.to_numeric(df['low'])
96
- if 'volume' in df.columns:
97
- df['volume'] = pd.to_numeric(df['volume'], errors='coerce').fillna(0)
98
- CACHE[cache_key] = df
99
- CACHE_TIMES[cache_key] = time.time()
100
- return df
101
- except Exception as e:
102
- print(f"⚠️ {symbol}: {e}")
103
- return None
104
-
105
- # ================= ИНДИКАТОРЫ =================
106
- def safe_rsi(close: pd.Series, period: int = 14) -> float:
107
- try:
108
- delta = close.diff()
109
- gain = delta.clip(lower=0).rolling(period, min_periods=period).mean()
110
- loss = (-delta.clip(upper=0)).rolling(period, min_periods=period).mean()
111
- g_val, l_val = gain.iloc[-1], loss.iloc[-1]
112
- if pd.notna(g_val) and pd.notna(l_val) and l_val > 0:
113
- rs = g_val / l_val
114
- return float(100 - (100 / (1 + rs)))
115
- return 50.0
116
  except:
117
- return 50.0
118
-
119
- def calculate_euphoria_index(df: pd.DataFrame) -> float:
120
- """Euphoria Index: 0-100, где >70 = эйфория."""
121
- if df is None or len(df) < 50:
122
- return 50.0
123
-
124
- close = df['close']
125
- volume = df['volume'] if 'volume' in df.columns else pd.Series([1]*len(df))
126
-
127
- score = 0.0
128
-
129
- # RSI на D1 (перекупленность)
130
- rsi = safe_rsi(close, 14)
131
- if rsi > 80:
132
- score += 30
133
- elif rsi > 70:
134
- score += 20
135
- elif rsi > 60:
136
- score += 10
137
-
138
- # Положение относительно SMA 50
139
- if len(close) >= 50:
140
- sma50 = close.rolling(50).mean().iloc[-1]
141
- price_vs_sma = ((close.iloc[-1] - sma50) / sma50) * 100
142
- if price_vs_sma > 20:
143
- score += 25
144
- elif price_vs_sma > 10:
145
- score += 15
146
- elif price_vs_sma > 5:
147
- score += 5
148
-
149
- # Объём (аномально высокий = эйфория)
150
- if len(volume) >= 20:
151
- vol_avg = volume.rolling(20).mean().iloc[-1]
152
- vol_ratio = volume.iloc[-1] / (vol_avg + 1e-10)
153
- if vol_ratio > 3:
154
- score += 20
155
- elif vol_ratio > 2:
156
- score += 10
157
-
158
- return min(100, score)
159
-
160
- def calculate_nvt_ratio(df: pd.DataFrame) -> float:
161
- """Network Value to Transactions — аналог NVT для крипты."""
162
- if df is None or len(df) < 30:
163
- return 50.0
164
-
165
- close = df['close']
166
- volume = df['volume'] if 'volume' in df.columns else pd.Series([1]*len(df))
167
-
168
- # Упрощённый NVT = MarketCap / Daily Volume
169
- market_cap = close.iloc[-1] * 120_000_000 # Грубая оценка supply
170
- daily_volume = volume.iloc[-24:].sum() if len(volume) >= 24 else volume.sum()
171
-
172
- if daily_volume > 0:
173
- nvt = market_cap / daily_volume
174
- else:
175
- nvt = 50
176
-
177
- # Нормализация (для ETH норма NVT ~ 30-80)
178
- if nvt > 150:
179
- nvt_signal = "EXTREME_OVERBOUGHT"
180
- nvt_score = 30
181
- elif nvt > 100:
182
- nvt_signal = "OVERBOUGHT"
183
- nvt_score = 20
184
- elif nvt < 30:
185
- nvt_signal = "OVERSOLD"
186
- nvt_score = -15
187
- else:
188
- nvt_signal = "NORMAL"
189
- nvt_score = 0
190
-
191
- return float(nvt_score)
192
-
193
- # ================= РЕЖИМ РЫНКА =================
194
- def detect_market_regime(df: pd.DataFrame, symbol: str) -> Dict[str, Any]:
195
- if df is None or len(df) < 50:
196
- return {"regime": "UNKNOWN", "score": 50, "bubble_risk": "UNKNOWN", "veto": False}
197
-
198
- close = df['close'].values
199
- high = df['high'].values
200
- low = df['low'].values
201
-
202
- # Волатильность
203
- returns = np.diff(np.log(close))
204
- volatility = float(np.std(returns[-24:])) if len(returns) >= 24 else 0.01
205
-
206
- # ADX (упрощённо через размах)
207
- recent_high = np.max(high[-20:])
208
- recent_low = np.min(low[-20:])
209
- range_pct = (recent_high - recent_low) / recent_low * 100
210
-
211
- # Euphoria Index
212
- euphoria = calculate_euphoria_index(df)
213
-
214
- # NVT (только для крипты)
215
- nvt_score = 0.0
216
- if "XAU" not in symbol:
217
- nvt_score = calculate_nvt_ratio(df)
218
-
219
- # RSI
220
- rsi = safe_rsi(pd.Series(close), 14)
221
-
222
- # Определение режима
223
- if euphoria > 70:
224
- regime = "BUBBLE"
225
- veto = True
226
- signal = "FORCE_WAIT"
227
- elif euphoria > 55:
228
- regime = "EUPHORIA"
229
- veto = False
230
- signal = "CAUTION"
231
- elif volatility > 0.03:
232
- regime = "VOLATILE"
233
- veto = False
234
- signal = "NEUTRAL"
235
- elif range_pct < 3:
236
- regime = "RANGE"
237
- veto = False
238
- signal = "NEUTRAL"
239
  else:
240
- regime = "TREND"
241
- veto = False
242
- signal = "NORMAL"
243
-
244
- regime_score = 50.0
245
- if regime == "BUBBLE":
246
- regime_score = 90
247
- elif regime == "EUPHORIA":
248
- regime_score = 70
249
- elif regime == "VOLATILE":
250
- regime_score = 55
251
- elif regime == "RANGE":
252
- regime_score = 30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  else:
254
- regime_score = 50
 
 
255
 
256
  return {
257
- "regime": regime,
258
- "regime_score": regime_score,
259
- "euphoria_index": round(euphoria, 1),
260
- "nvt_score": round(nvt_score, 1),
261
- "rsi_14": round(rsi, 1),
262
- "volatility_24h_pct": round(volatility * 100, 3),
263
- "range_20_pct": round(range_pct, 1),
264
- "bubble_risk": "HIGH" if regime == "BUBBLE" else "ELEVATED" if regime == "EUPHORIA" else "LOW",
265
- "veto": veto,
266
- "signal": signal
267
  }
268
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  # ================= АНАЛИЗ ИЗ MT5 =================
270
  def analyze_from_mt5(mt5_features: Dict[str, Any]) -> Optional[Dict[str, Any]]:
271
  try:
272
  score = 50.0
273
- veto = False
 
 
 
 
 
 
 
274
 
275
  rsi = mt5_features.get('H1_rsi', 50)
276
- if isinstance(rsi, (int, float)):
277
- if rsi > 85:
278
- veto = True
279
- regime = "BUBBLE"
280
- score = 90
281
- elif rsi > 75:
282
- regime = "EUPHORIA"
283
- score = 70
284
- elif rsi < 20:
285
- regime = "CAPITULATION"
286
- score = 10
287
- else:
288
- regime = "NORMAL"
289
 
290
- atr_pct = mt5_features.get('H1_atr_pct', 1)
291
- if isinstance(atr_pct, (int, float)) and atr_pct > 4:
292
- regime = "VOLATILE"
293
- score = max(score, 60)
 
 
 
 
 
 
 
 
 
 
294
 
295
  return {
296
- "regime": regime,
297
- "regime_score": score,
298
- "veto": veto,
299
- "euphoria_index": score,
300
  "source": "MT5"
301
  }
302
  except:
303
  return None
304
 
305
  # ================= ГЛАВНЫЙ СИГНАЛ =================
306
- def get_regime_signal(symbol: str = "XAU/USD") -> Dict[str, Any]:
307
  start = time.time()
308
 
309
  # Проверка MT5
310
- if symbol in FEATURES_STORE:
311
- fs = FEATURES_STORE[symbol]
312
- age = time.time() - fs.get("timestamp", 0)
313
- if age < MT5_MAX_AGE_SEC:
314
- mt5_features = fs.get("features", {})
315
- if mt5_features:
316
- mt5_result = analyze_from_mt5(mt5_features)
317
- if mt5_result:
318
- direction = "WAIT" if mt5_result["veto"] else "NEUTRAL"
319
- result = {
320
- "space": "space_28_regime",
321
- "timestamp": int(time.time()),
322
- "symbol": symbol,
323
- "signal": {
324
- "direction": direction,
325
- "confidence": mt5_result["regime_score"] / 100,
326
- "veto": mt5_result["veto"]
327
- },
328
- "regime_analysis": {
329
- "regime": mt5_result["regime"],
330
- "regime_score": mt5_result["regime_score"],
331
- "euphoria_index": mt5_result["euphoria_index"],
332
- "veto": mt5_result["veto"]
333
- },
334
- "data_source": "MT5",
335
- "meta": {"latency_ms": int((time.time() - start) * 1000)}
336
- }
337
- send_to_arbiter(result)
338
- print(f"🫧 REGIME {symbol}: {mt5_result['regime']} | Veto={mt5_result['veto']} (MT5)")
339
- return result
340
 
341
  # Fallback
342
- df = fetch_historical(symbol, "1h", 200)
343
- regime_data = detect_market_regime(df, symbol)
344
-
345
- direction = "WAIT" if regime_data["veto"] else "NEUTRAL"
346
- confidence = regime_data["regime_score"] / 100
347
-
348
  latency = int((time.time() - start) * 1000)
349
 
350
  result = {
351
- "space": "space_28_regime",
352
  "timestamp": int(time.time()),
353
- "symbol": symbol,
354
- "signal": {
355
- "direction": direction,
356
- "confidence": round(confidence, 4),
357
- "veto": regime_data["veto"],
358
- "veto_reason": "BUBBLE_DETECTED" if regime_data["veto"] else None
359
  },
360
- "regime_analysis": regime_data,
361
- "data_source": "API",
 
 
 
 
 
 
362
  "meta": {"latency_ms": latency}
363
  }
364
 
365
  send_to_arbiter(result)
366
- print(f"🫧 REGIME v1 {symbol}: {regime_data['regime']} | Euphoria={regime_data['euphoria_index']:.0f} | Veto={regime_data['veto']}")
367
  return result
368
 
369
  # ================= KEEP-ALIVE =================
@@ -378,57 +418,37 @@ def keep_alive():
378
  threading.Thread(target=keep_alive, daemon=True).start()
379
 
380
  # ================= FASTAPI =================
381
- app = FastAPI(title="TOMIRIS SPACE 28 v1.0 — MARKET REGIME & BUBBLE DETECTOR")
382
 
383
  @app.get("/health")
384
  @app.head("/health")
385
  async def health():
386
  return {
387
- "space": "Space 28 - Market Regime & Bubble Detector v1.0",
388
  "status": "operational",
389
  "symbols": SYMBOLS,
390
- "features": ["Euphoria Index", "NVT Ratio", "RSI Extremes", "Bubble VETO"],
391
- "bubble_history": len(BUBBLE_HISTORY)
392
  }
393
 
394
  @app.get("/consilium")
395
- async def consilium(symbol: str = Query("XAU/USD")):
396
- if symbol not in SYMBOLS:
397
- return {"error": f"Unsupported: {symbol}"}
398
- return get_regime_signal(symbol)
399
-
400
- @app.get("/regime/{symbol}")
401
- async def regime(symbol: str):
402
- if symbol not in SYMBOLS:
403
- return {"error": f"Unsupported: {symbol}"}
404
- df = fetch_historical(symbol)
405
- if df is None:
406
- return {"error": "no_data"}
407
- return detect_market_regime(df, symbol)
408
-
409
- @app.get("/euphoria/{symbol}")
410
- async def euphoria(symbol: str):
411
- if symbol not in SYMBOLS:
412
- return {"error": f"Unsupported: {symbol}"}
413
- df = fetch_historical(symbol)
414
- if df is None:
415
- return {"error": "no_data"}
416
- return {
417
- "symbol": symbol,
418
- "euphoria_index": calculate_euphoria_index(df)
419
- }
420
 
421
- @app.get("/nvt/{symbol}")
422
- async def nvt(symbol: str):
423
- if symbol not in ["ETH/USD", "SOL/USD"]:
424
- return {"error": "NVT доступен только для крипты"}
425
- df = fetch_historical(symbol)
426
- if df is None:
427
- return {"error": "no_data"}
428
- return {
429
- "symbol": symbol,
430
- "nvt_score": calculate_nvt_ratio(df)
431
- }
 
 
 
432
 
433
  @app.post("/features")
434
  async def receive_features(data: Dict[str, Any]):
@@ -441,6 +461,6 @@ async def receive_features(data: Dict[str, Any]):
441
  print(f"📥 MT5 {symbol}: {len(data.get('features', {}))} признаков")
442
  return {"status": "ok"}
443
 
444
- print(f"🚀 SPACE 28 v1.0 — MARKET REGIME & BUBBLE DETECTOR ЗАПУЩЕН!")
445
- print(f"🫧 Детектор: Euphoria Index | NVT Ratio | RSI | Режим рынка | ВЕТО при пузыре")
446
  print(f"✅ Готов к бою!")
 
7
 
8
  REQUIRED_PACKAGES = {
9
  'numpy': 'numpy',
 
10
  'requests': 'requests'
11
  }
12
 
 
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')
 
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 =================
 
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]):
 
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"✅ Готов к бою!")