tomirisg25 commited on
Commit
e5ba629
·
verified ·
1 Parent(s): 3c745b7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +319 -276
app.py CHANGED
@@ -1,13 +1,14 @@
1
  # ============================================
2
  # АВТО-УСТАНОВКА ПАКЕТОВ
3
  # ============================================
4
- import subprocess
5
- import sys
6
- import importlib
7
 
8
  REQUIRED_PACKAGES = {
9
  'numpy': 'numpy',
10
  'pandas': 'pandas',
 
 
 
11
  'requests': 'requests'
12
  }
13
 
@@ -20,409 +21,451 @@ 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
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')
 
41
 
42
  # ================= КОНФИГУРАЦИЯ =================
43
  SYMBOLS = ["XAU/USD", "ETH/USD", "SOL/USD"]
 
 
 
 
 
 
 
 
 
 
 
44
 
45
- SPACE_18_ARBITER = "https://tomiris-ai-name6-6.hf.space"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
- TWELVE_KEYS = [
48
- "e3740c072fda4fe8b8539d40b07e445e",
49
- "58e67e0008e24161ac9b1671b7c2d2d0"
50
- ]
 
 
 
 
 
51
 
52
- CACHE = {}
53
- CACHE_TIMES = {}
54
- FEATURES_STORE = {}
55
- MT5_MAX_AGE_SEC = 300
56
- BUBBLE_HISTORY = deque(maxlen=200)
57
 
58
- twelve_counter = 0
59
- api_lock = threading.Lock()
 
 
 
 
60
 
61
- def get_next_key():
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):
 
 
 
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, tf="1h", count=200):
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, period=14):
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):
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 = safe_rsi(close, 14)
130
  if rsi > 80:
131
- score += 30
132
  elif rsi > 70:
133
- score += 20
134
  elif rsi > 60:
135
- score += 10
 
 
 
 
136
 
 
137
  if len(close) >= 50:
138
  sma50 = close.rolling(50).mean().iloc[-1]
139
- price_vs_sma = ((close.iloc[-1] - sma50) / sma50) * 100
140
- if price_vs_sma > 20:
141
- score += 25
142
- elif price_vs_sma > 10:
143
- score += 15
144
- elif price_vs_sma > 5:
145
  score += 5
 
 
146
 
 
147
  if len(volume) >= 20:
148
- vol_avg = volume.rolling(20).mean().iloc[-1]
149
- vol_ratio = volume.iloc[-1] / (vol_avg + 1e-10)
150
  if vol_ratio > 3:
151
- score += 20
152
  elif vol_ratio > 2:
153
- score += 10
154
-
155
- return min(100, score)
156
 
157
- def calculate_nvt_ratio(df):
158
- """Network Value to Transactions — аналог NVT для крипты."""
159
- if df is None or len(df) < 30:
160
- return 50.0
161
-
162
- close = df['close']
163
- volume = df['volume'] if 'volume' in df.columns else pd.Series([1]*len(df))
164
 
165
- market_cap = close.iloc[-1] * 120_000_000
166
- daily_volume = volume.iloc[-24:].sum() if len(volume) >= 24 else volume.sum()
 
 
 
 
167
 
168
- if daily_volume > 0:
169
- nvt = market_cap / daily_volume
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  else:
171
- nvt = 50
172
-
173
- if nvt > 150:
174
- nvt_score = 30
175
- elif nvt > 100:
176
- nvt_score = 20
177
- elif nvt < 30:
178
- nvt_score = -15
 
 
 
 
179
  else:
180
- nvt_score = 0
181
 
182
- return float(nvt_score)
 
 
 
 
183
 
184
  # ================= РЕЖИМ РЫНКА =================
185
- def detect_market_regime(df, symbol):
 
186
  if df is None or len(df) < 50:
187
- return {"regime": "UNKNOWN", "score": 50, "bubble_risk": "UNKNOWN", "veto": False}
188
-
189
- close = df['close'].values
190
- high = df['high'].values
191
- low = df['low'].values
192
-
193
- returns = np.diff(np.log(close))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  volatility = float(np.std(returns[-24:])) if len(returns) >= 24 else 0.01
195
 
196
- recent_high = np.max(high[-20:])
197
- recent_low = np.min(low[-20:])
198
- range_pct = (recent_high - recent_low) / recent_low * 100
199
-
200
- euphoria = calculate_euphoria_index(df)
201
-
202
  nvt_score = 0.0
203
- if "XAU" not in symbol:
204
- nvt_score = calculate_nvt_ratio(df)
205
-
206
- rsi = safe_rsi(pd.Series(close), 14)
207
 
208
- if euphoria > 70:
 
209
  regime = "BUBBLE"
 
210
  veto = True
211
  signal = "FORCE_WAIT"
212
  elif euphoria > 55:
213
  regime = "EUPHORIA"
 
214
  veto = False
215
  signal = "CAUTION"
216
  elif volatility > 0.03:
217
  regime = "VOLATILE"
 
218
  veto = False
219
  signal = "NEUTRAL"
220
- elif range_pct < 3:
 
 
 
 
 
221
  regime = "RANGE"
 
222
  veto = False
223
  signal = "NEUTRAL"
224
  else:
225
  regime = "TREND"
 
226
  veto = False
227
  signal = "NORMAL"
228
 
229
- regime_score = 50.0
230
- if regime == "BUBBLE":
231
- regime_score = 90
232
- elif regime == "EUPHORIA":
233
- regime_score = 70
234
- elif regime == "VOLATILE":
235
- regime_score = 55
236
- elif regime == "RANGE":
237
- regime_score = 30
238
- else:
239
- regime_score = 50
240
 
241
  return {
242
  "regime": regime,
243
- "regime_score": regime_score,
244
  "euphoria_index": round(euphoria, 1),
245
  "nvt_score": round(nvt_score, 1),
246
- "rsi_14": round(rsi, 1),
247
  "volatility_24h_pct": round(volatility * 100, 3),
248
- "range_20_pct": round(range_pct, 1),
249
- "bubble_risk": "HIGH" if regime == "BUBBLE" else "ELEVATED" if regime == "EUPHORIA" else "LOW",
250
  "veto": veto,
251
  "signal": signal
252
  }
253
 
254
- # ================= АНАЛИЗ ИЗ MT5 =================
255
- def analyze_from_mt5(mt5_features):
256
- try:
257
- score = 50.0
258
- veto = False
259
- regime = "NORMAL"
260
-
261
- rsi = mt5_features.get('H1_rsi', 50)
262
- if isinstance(rsi, (int, float)):
263
- if rsi > 85:
264
- veto = True
265
- regime = "BUBBLE"
266
- score = 90
267
- elif rsi > 75:
268
- regime = "EUPHORIA"
269
- score = 70
270
- elif rsi < 20:
271
- regime = "CAPITULATION"
272
- score = 10
273
-
274
- atr_pct = mt5_features.get('H1_atr_pct', 1)
275
- if isinstance(atr_pct, (int, float)) and atr_pct > 4:
276
- regime = "VOLATILE"
277
- score = max(score, 60)
278
-
279
- return {
280
- "regime": regime,
281
- "regime_score": score,
282
- "veto": veto,
283
- "euphoria_index": score,
284
- "source": "MT5"
285
- }
286
- except:
287
- return None
288
-
289
  # ================= ГЛАВНЫЙ СИГНАЛ =================
290
- def get_regime_signal(symbol="XAU/USD"):
291
  start = time.time()
292
-
293
- if symbol in FEATURES_STORE:
294
- fs = FEATURES_STORE[symbol]
295
- age = time.time() - fs.get("timestamp", 0)
296
- if age < MT5_MAX_AGE_SEC:
297
- mt5_features = fs.get("features", {})
298
- if mt5_features:
299
- mt5_result = analyze_from_mt5(mt5_features)
300
- if mt5_result:
301
- direction = "WAIT" if mt5_result["veto"] else "NEUTRAL"
302
- result = {
303
- "space": "space_28_regime",
304
- "timestamp": int(time.time()),
305
- "symbol": symbol,
306
- "signal": {
307
- "direction": direction,
308
- "confidence": mt5_result["regime_score"] / 100,
309
- "veto": mt5_result["veto"]
310
- },
311
- "regime_analysis": {
312
- "regime": mt5_result["regime"],
313
- "regime_score": mt5_result["regime_score"],
314
- "euphoria_index": mt5_result["euphoria_index"],
315
- "veto": mt5_result["veto"]
316
- },
317
- "data_source": "MT5",
318
- "meta": {"latency_ms": int((time.time() - start) * 1000)}
319
- }
320
- send_to_arbiter(result)
321
- print(f"🫧 REGIME {symbol}: {mt5_result['regime']} | Veto={mt5_result['veto']} (MT5)")
322
- return result
323
-
324
- df = fetch_historical(symbol, "1h", 200)
325
- regime_data = detect_market_regime(df, symbol)
326
-
327
- direction = "WAIT" if regime_data["veto"] else "NEUTRAL"
328
- confidence = regime_data["regime_score"] / 100
329
-
330
  latency = int((time.time() - start) * 1000)
331
 
 
332
  result = {
333
  "space": "space_28_regime",
334
  "timestamp": int(time.time()),
335
  "symbol": symbol,
336
  "signal": {
337
  "direction": direction,
338
- "confidence": round(confidence, 4),
339
  "veto": regime_data["veto"],
340
  "veto_reason": "BUBBLE_DETECTED" if regime_data["veto"] else None
341
  },
342
  "regime_analysis": regime_data,
343
- "data_source": "API",
344
- "meta": {"latency_ms": latency}
345
  }
346
 
347
- send_to_arbiter(result)
348
- print(f"🫧 REGIME v1 {symbol}: {regime_data['regime']} | Euphoria={regime_data['euphoria_index']:.0f} | Veto={regime_data['veto']}")
 
 
 
 
 
 
 
 
 
 
349
  return result
350
 
351
- # ================= KEEP-ALIVE =================
352
- def keep_alive():
353
- while True:
354
- time.sleep(840)
355
- try:
356
- requests.get("http://localhost:7860/health", timeout=5)
357
- except:
358
- pass
359
 
360
- threading.Thread(target=keep_alive, daemon=True).start()
 
361
 
362
- # ================= FASTAPI =================
363
- app = FastAPI(title="TOMIRIS SPACE 28 v1.0 — MARKET REGIME & BUBBLE DETECTOR")
364
 
365
  @app.get("/health")
366
- @app.head("/health")
367
  async def health():
368
- return {
369
- "space": "Space 28 - Market Regime & Bubble Detector v1.0",
370
- "status": "operational",
371
- "symbols": SYMBOLS,
372
- "features": ["Euphoria Index", "NVT Ratio", "RSI Extremes", "Bubble VETO"],
373
- "bubble_history": len(BUBBLE_HISTORY)
374
- }
375
 
376
  @app.get("/consilium")
377
  async def consilium(symbol: str = Query("XAU/USD")):
378
  if symbol not in SYMBOLS:
379
- return {"error": f"Unsupported: {symbol}"}
380
- return get_regime_signal(symbol)
381
 
382
  @app.get("/regime/{symbol}")
383
  async def regime(symbol: str):
384
  if symbol not in SYMBOLS:
385
- return {"error": f"Unsupported: {symbol}"}
386
- df = fetch_historical(symbol)
387
- if df is None:
388
- return {"error": "no_data"}
389
- return detect_market_regime(df, symbol)
390
 
391
  @app.get("/euphoria/{symbol}")
392
  async def euphoria(symbol: str):
393
  if symbol not in SYMBOLS:
394
- return {"error": f"Unsupported: {symbol}"}
395
- df = fetch_historical(symbol)
396
  if df is None:
397
- return {"error": "no_data"}
398
- return {
399
- "symbol": symbol,
400
- "euphoria_index": calculate_euphoria_index(df)
401
- }
 
 
 
 
 
 
402
 
403
  @app.get("/nvt/{symbol}")
404
  async def nvt(symbol: str):
405
  if symbol not in ["ETH/USD", "SOL/USD"]:
406
  return {"error": "NVT доступен только для крипты"}
407
- df = fetch_historical(symbol)
408
  if df is None:
409
- return {"error": "no_data"}
410
- return {
411
- "symbol": symbol,
412
- "nvt_score": calculate_nvt_ratio(df)
413
- }
414
 
415
- @app.post("/features")
416
- async def receive_features(data: Dict[str, Any]):
417
- symbol = data.get("symbol", "XAU/USD")
418
- FEATURES_STORE[symbol] = {
419
- "features": data.get("features", {}),
420
- "price": data.get("price", 0.0),
421
- "timestamp": time.time()
422
- }
423
- print(f"📥 MT5 {symbol}: {len(data.get('features', {}))} признаков")
424
- return {"status": "ok"}
425
 
426
- print(f"🚀 SPACE 28 v1.0 — MARKET REGIME & BUBBLE DETECTOR ЗАПУЩЕН!")
427
- print(f"🫧 Детектор: Euphoria Index | NVT Ratio | RSI | Режим рынка | ВЕТО при пузыре")
428
- 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 28 v2.0 — MARKET REGIME & BUBBLE SENTINEL (Async, No MT5, Real NVT)
25
  # ============================================
26
+ import os, time, json, logging, asyncio
 
 
 
 
 
27
  from typing import Dict, Any, List, Optional
28
+ from datetime import datetime, timezone
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("Space28_RegimeBubble")
37
 
38
  # ================= КОНФИГУРАЦИЯ =================
39
  SYMBOLS = ["XAU/USD", "ETH/USD", "SOL/USD"]
40
+ HUB_URL = os.getenv("SPACE17_URL", "https://tomiris-ai-name5-5.hf.space")
41
+ ARBITER_URL = os.getenv("SPACE18_URL", "https://tomiris-ai-name6-6.hf.space")
42
+ SPACE9_URL = os.getenv("SPACE9_URL", "https://nuxotetotnicksvoboden-name3.hf.space") # On-Chain Analytics
43
+ SPACE22_URL = os.getenv("SPACE22_URL", "https://tomirisai80-tomirisanal4.hf.space") # Sentiment
44
+ SPACE26_URL = os.getenv("SPACE26_URL", "https://tomirisg25-tomirisgold2.hf.space") # Derivatives
45
+ TWELVE_DATA_KEY = os.getenv("TWELVE_DATA_KEY", "e3740c072fda4fe8b8539d40b07e445e")
46
+
47
+ BUBBLE_HISTORY_FILE = "bubble_history.json"
48
+ CACHE_TTL = {
49
+ "candles": 300, "onchain": 600, "sentiment": 300, "derivatives": 120
50
+ }
51
 
52
+ # ================= HTTP КЛИЕНТ =================
53
+ http_client = httpx.AsyncClient(timeout=20.0)
54
+
55
+ # ================= ГЛОБАЛЬНЫЙ КЭШ И BREAKER =================
56
+ cache_store = {}
57
+ cache_times = {}
58
+ CIRCUIT_BREAKER = {}
59
+
60
+ def breaker_open(name: str) -> bool:
61
+ info = CIRCUIT_BREAKER.get(name)
62
+ if not info: return False
63
+ if info["fails"] < 3: return False
64
+ if time.time() - info["last_fail"] > 600:
65
+ CIRCUIT_BREAKER[name] = {"fails": 0, "last_fail": 0}
66
+ return False
67
+ return True
68
+
69
+ def breaker_record(name: str, success: bool):
70
+ info = CIRCUIT_BREAKER.get(name, {"fails": 0, "last_fail": 0})
71
+ if success:
72
+ info["fails"] = 0
73
+ else:
74
+ info["fails"] += 1
75
+ info["last_fail"] = time.time()
76
+ CIRCUIT_BREAKER[name] = info
77
 
78
+ # История пузырей
79
+ if os.path.exists(BUBBLE_HISTORY_FILE):
80
+ try:
81
+ with open(BUBBLE_HISTORY_FILE) as f:
82
+ BUBBLE_HISTORY = deque(json.load(f), maxlen=500)
83
+ except:
84
+ BUBBLE_HISTORY = deque(maxlen=500)
85
+ else:
86
+ BUBBLE_HISTORY = deque(maxlen=500)
87
 
88
+ def save_bubble_history():
89
+ with open(BUBBLE_HISTORY_FILE, 'w') as f:
90
+ json.dump(list(BUBBLE_HISTORY), f)
 
 
91
 
92
+ # ================= ЗАГРУЗКА ДАННЫХ =================
93
+ async def fetch_candles(symbol: str, tf: str = "1h", count: int = 200) -> Optional[pd.DataFrame]:
94
+ """Получает свечи из Hub."""
95
+ cache_key = f"candles_{symbol}_{tf}_{count}"
96
+ if cache_key in cache_store and time.time() - cache_times.get(cache_key, 0) < CACHE_TTL["candles"]:
97
+ return cache_store[cache_key]
98
 
99
+ if breaker_open("hub"): return None
100
+ try:
101
+ r = await http_client.get(f"{HUB_URL}/candles", params={"symbol": symbol, "interval": tf, "limit": count})
102
+ if r.status_code == 200:
103
+ data = r.json().get("candles", [])
104
+ if data:
105
+ df = pd.DataFrame(data)
106
+ df.rename(columns={"o": "open", "h": "high", "l": "low", "c": "close", "v": "volume", "t": "timestamp"}, inplace=True)
107
+ df["close"] = pd.to_numeric(df["close"], errors="coerce")
108
+ df["high"] = pd.to_numeric(df["high"], errors="coerce")
109
+ df["low"] = pd.to_numeric(df["low"], errors="coerce")
110
+ if "volume" in df.columns:
111
+ df["volume"] = pd.to_numeric(df["volume"], errors="coerce").fillna(0)
112
+ breaker_record("hub", True)
113
+ cache_store[cache_key] = df
114
+ cache_times[cache_key] = time.time()
115
+ return df
116
+ breaker_record("hub", False)
117
+ except:
118
+ breaker_record("hub", False)
119
+ return None
120
 
121
+ async def fetch_onchain_volume(symbol: str) -> Optional[float]:
122
+ """Пытается получить on‑chain объём транзакций из Space 9."""
123
+ if not SPACE9_URL or breaker_open("space9"):
124
+ return None
125
  try:
126
+ # Ожидаем, что Space 9 отдаёт что-то вроде {"network": {"tx_volume_24h": ...}}
127
+ r = await http_client.get(f"{SPACE9_URL}/consilium?symbol={symbol}")
128
+ if r.status_code == 200:
129
+ data = r.json()
130
+ # Ищем поле с объёмом транзакций (может быть в разных местах)
131
+ metrics = data.get("onchain_analysis", {}).get("metrics", {})
132
+ network = metrics.get("network", {})
133
+ vol = network.get("tx_volume_24h") or network.get("total_volume_24h")
134
+ if vol:
135
+ breaker_record("space9", True)
136
+ return float(vol)
137
  except:
138
+ breaker_record("space9", False)
139
+ return None
140
 
141
+ async def fetch_sentiment_signal(symbol: str) -> Optional[Dict]:
142
+ """Получает социальный сентимент из Space 22."""
143
+ if not SPACE22_URL or breaker_open("space22"): return None
144
+ try:
145
+ r = await http_client.get(f"{SPACE22_URL}/sentiment/{symbol}")
146
+ if r.status_code == 200:
147
+ data = r.json()
148
+ breaker_record("space22", True)
149
+ return data
150
+ except:
151
+ breaker_record("space22", False)
152
+ return None
153
 
154
+ async def fetch_derivatives(symbol: str) -> Optional[Dict]:
155
+ """Получает Funding Rate и OI из Space 26."""
156
+ if not SPACE26_URL or breaker_open("space26"): return None
157
  try:
158
+ # Предполагаем, что Space 26 отдаёт /consilium с метриками
159
+ r = await http_client.get(f"{SPACE26_URL}/consilium?symbol={symbol}")
 
 
160
  if r.status_code == 200:
161
  data = r.json()
162
+ deriv = data.get("derivative_analysis", {}).get("metrics", {})
163
+ breaker_record("space26", True)
164
+ return deriv
165
+ except:
166
+ breaker_record("space26", False)
 
 
 
 
 
 
 
167
  return None
168
 
169
  # ================= ИНДИКАТОРЫ =================
170
+ def safe_rsi(close: pd.Series, period: int = 14) -> float:
171
  try:
172
  delta = close.diff()
173
  gain = delta.clip(lower=0).rolling(period, min_periods=period).mean()
174
  loss = (-delta.clip(upper=0)).rolling(period, min_periods=period).mean()
175
+ rs = gain / (loss + 1e-10)
176
+ return float(100 - (100 / (1 + rs.iloc[-1])))
 
 
 
177
  except:
178
  return 50.0
179
 
180
+ def calculate_euphoria_index(df: pd.DataFrame, funding_signal: Optional[str] = None,
181
+ oi_change: Optional[float] = None, sentiment_fear: Optional[float] = None) -> float:
182
+ """Euphoria Index 0-100, учитывающий RSI, цену, объём, фондирование, OI, сентимент."""
183
+ if df is None or len(df) < 20:
184
  return 50.0
 
185
  close = df['close']
186
+ volume = df['volume'] if 'volume' in df.columns else pd.Series([1.0]*len(df))
187
 
188
  score = 0.0
189
 
190
+ # RSI
191
  rsi = safe_rsi(close, 14)
192
  if rsi > 80:
193
+ score += 25
194
  elif rsi > 70:
195
+ score += 18
196
  elif rsi > 60:
197
+ score += 8
198
+ elif rsi < 30:
199
+ score -= 15
200
+ elif rsi < 20:
201
+ score -= 20
202
 
203
+ # Цена относительно SMA50
204
  if len(close) >= 50:
205
  sma50 = close.rolling(50).mean().iloc[-1]
206
+ dev = (close.iloc[-1] - sma50) / sma50 * 100
207
+ if dev > 20:
208
+ score += 20
209
+ elif dev > 10:
210
+ score += 12
211
+ elif dev > 5:
212
  score += 5
213
+ elif dev < -20:
214
+ score -= 15
215
 
216
+ # Объём
217
  if len(volume) >= 20:
218
+ avg_vol = volume.rolling(20).mean().iloc[-1]
219
+ vol_ratio = volume.iloc[-1] / (avg_vol + 1e-10)
220
  if vol_ratio > 3:
221
+ score += 15
222
  elif vol_ratio > 2:
223
+ score += 8
 
 
224
 
225
+ # Funding (если есть)
226
+ if funding_signal:
227
+ if funding_signal == 'EXTREME_LONG' or funding_signal == 'BEARISH':
228
+ score += 15
229
+ elif funding_signal == 'BULLISH':
230
+ score -= 10
 
231
 
232
+ # OI изменение
233
+ if oi_change:
234
+ if oi_change > 10:
235
+ score += 10
236
+ elif oi_change < -10:
237
+ score -= 5
238
 
239
+ # Сентимент (0 - страх, 100 - жадность)
240
+ if sentiment_fear is not None:
241
+ greed = 100 - sentiment_fear
242
+ if greed > 70:
243
+ score += 10
244
+ elif greed < 30:
245
+ score -= 10
246
+
247
+ return max(0.0, min(100.0, score))
248
+
249
+ def calculate_nvt_ratio(symbol: str, df: pd.DataFrame, onchain_volume: Optional[float] = None) -> float:
250
+ """
251
+ Настоящий NVT: Market Cap / On‑Chain Transaction Volume.
252
+ Если onchain_volume недоступен, используем биржевой объём как fallback (pseudo NVT).
253
+ Возвращает Z‑score относительно нормы.
254
+ """
255
+ if df is None or len(df) < 24:
256
+ return 0.0
257
+ close = df['close'].iloc[-1]
258
+ # Циркулирующее предложение (приблизительно)
259
+ supply = {"ETH/USD": 120_000_000, "SOL/USD": 440_000_000, "XAU/USD": 1}.get(symbol, 1)
260
+ market_cap = close * supply
261
+
262
+ if onchain_volume and onchain_volume > 0:
263
+ nvt = market_cap / onchain_volume
264
  else:
265
+ # Pseudo NVT: используем биржевой объём за 24 часа
266
+ volume = df['volume'] if 'volume' in df.columns else pd.Series([1.0]*len(df))
267
+ daily_volume = volume.iloc[-24:].sum() if len(volume) >= 24 else volume.sum()
268
+ if daily_volume <= 0:
269
+ return 0.0
270
+ nvt = market_cap / daily_volume
271
+
272
+ # Нормальный диапазон NVT: ~30-100 (для ETH), ~50-150 (для SOL)
273
+ if "ETH" in symbol:
274
+ normal_low, normal_high = 30, 100
275
+ elif "SOL" in symbol:
276
+ normal_low, normal_high = 50, 150
277
  else:
278
+ return 0.0 # для золота не считаем
279
 
280
+ if nvt > normal_high:
281
+ return min(30, (nvt - normal_high) / 10) # до +30
282
+ elif nvt < normal_low:
283
+ return max(-20, (nvt - normal_low) / 10) # до -20
284
+ return 0.0
285
 
286
  # ================= РЕЖИМ РЫНКА =================
287
+ async def detect_market_regime(symbol: str) -> Dict[str, Any]:
288
+ df = await fetch_candles(symbol, "1h", 200)
289
  if df is None or len(df) < 50:
290
+ return {"regime": "UNKNOWN", "bubble_probability": 0, "veto": False, "euphoria_index": 50}
291
+
292
+ # Дополнительные данные
293
+ onchain_vol = None
294
+ if symbol != "XAU/USD":
295
+ onchain_vol = await fetch_onchain_volume(symbol)
296
+ deriv = None
297
+ if symbol != "XAU/USD":
298
+ deriv = await fetch_derivatives(symbol)
299
+ sent = await fetch_sentiment_signal(symbol)
300
+
301
+ funding_signal = None
302
+ oi_change = None
303
+ if deriv:
304
+ funding = deriv.get("funding_rate", {})
305
+ funding_signal = funding.get("signal")
306
+ oi = deriv.get("open_interest", {})
307
+ oi_change = oi.get("change_pct")
308
+
309
+ fear_ratio = None
310
+ if sent:
311
+ # Ищем fear_ratio в ответе (поле может называться по-разному)
312
+ reddit = sent.get("metrics", {}).get("reddit", {})
313
+ fear_ratio = reddit.get("fear_ratio") or sent.get("fear_ratio")
314
+
315
+ close = df['close'].astype(float)
316
+ returns = np.diff(np.log(close.values))
317
  volatility = float(np.std(returns[-24:])) if len(returns) >= 24 else 0.01
318
 
319
+ euphoria = calculate_euphoria_index(df, funding_signal, oi_change, fear_ratio)
 
 
 
 
 
320
  nvt_score = 0.0
321
+ if symbol != "XAU/USD":
322
+ nvt_score = calculate_nvt_ratio(symbol, df, onchain_vol)
 
 
323
 
324
+ # Определяем режим
325
+ if euphoria > 70 or nvt_score > 20:
326
  regime = "BUBBLE"
327
+ bubble_probability = min(100, euphoria + nvt_score * 2)
328
  veto = True
329
  signal = "FORCE_WAIT"
330
  elif euphoria > 55:
331
  regime = "EUPHORIA"
332
+ bubble_probability = euphoria
333
  veto = False
334
  signal = "CAUTION"
335
  elif volatility > 0.03:
336
  regime = "VOLATILE"
337
+ bubble_probability = 40
338
  veto = False
339
  signal = "NEUTRAL"
340
+ elif close.iloc[-1] < close.iloc[-50] * 0.8 and euphoria < 30:
341
+ regime = "CAPITULATION"
342
+ bubble_probability = 10
343
+ veto = False
344
+ signal = "NORMAL"
345
+ elif abs(close.iloc[-1] - close.iloc[-20]) / close.iloc[-20] < 0.02:
346
  regime = "RANGE"
347
+ bubble_probability = 20
348
  veto = False
349
  signal = "NEUTRAL"
350
  else:
351
  regime = "TREND"
352
+ bubble_probability = max(0, min(100, 50 + (close.iloc[-1] > close.iloc[-50] and 15 or -15)))
353
  veto = False
354
  signal = "NORMAL"
355
 
356
+ # Сохраняем в историю пузырей
357
+ BUBBLE_HISTORY.append({
358
+ "timestamp": datetime.now(timezone.utc).isoformat(),
359
+ "symbol": symbol,
360
+ "regime": regime,
361
+ "bubble_probability": bubble_probability,
362
+ "euphoria": euphoria
363
+ })
364
+ save_bubble_history()
 
 
365
 
366
  return {
367
  "regime": regime,
368
+ "bubble_probability": round(bubble_probability, 1),
369
  "euphoria_index": round(euphoria, 1),
370
  "nvt_score": round(nvt_score, 1),
371
+ "rsi_14": round(safe_rsi(close, 14), 1),
372
  "volatility_24h_pct": round(volatility * 100, 3),
 
 
373
  "veto": veto,
374
  "signal": signal
375
  }
376
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
377
  # ================= ГЛАВНЫЙ СИГНАЛ =================
378
+ async def get_regime_signal(symbol: str = "XAU/USD") -> Dict[str, Any]:
379
  start = time.time()
380
+ regime_data = await detect_market_regime(symbol)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
381
  latency = int((time.time() - start) * 1000)
382
 
383
+ direction = "WAIT" if regime_data["veto"] else "NEUTRAL"
384
  result = {
385
  "space": "space_28_regime",
386
  "timestamp": int(time.time()),
387
  "symbol": symbol,
388
  "signal": {
389
  "direction": direction,
390
+ "confidence": regime_data["bubble_probability"] / 100 if regime_data["veto"] else 0.0,
391
  "veto": regime_data["veto"],
392
  "veto_reason": "BUBBLE_DETECTED" if regime_data["veto"] else None
393
  },
394
  "regime_analysis": regime_data,
395
+ "latency_ms": latency
 
396
  }
397
 
398
+ # Отправка в Arbiter
399
+ try:
400
+ await http_client.post(f"{ARBITER_URL}/log_signal", json={
401
+ "space": "space_28_regime",
402
+ "symbol": symbol,
403
+ "signal": result["signal"],
404
+ "regime_details": regime_data
405
+ })
406
+ except:
407
+ pass
408
+
409
+ logger.info(f"🫧 Regime {symbol}: {regime_data['regime']} bubble_prob={regime_data['bubble_probability']:.1f} veto={regime_data['veto']}")
410
  return result
411
 
412
+ # ================= FASTAPI =================
413
+ app = FastAPI(title="Tomiris Space 28 v2.0 — Market Regime & Bubble Sentinel")
 
 
 
 
 
 
414
 
415
+ @app.on_event("startup")
416
+ async def startup(): pass
417
 
418
+ @app.on_event("shutdown")
419
+ async def shutdown(): await http_client.aclose()
420
 
421
  @app.get("/health")
 
422
  async def health():
423
+ return {"status": "operational", "version": "2.0", "async": True, "no_mt5": True,
424
+ "features": ["Real NVT", "Composite Bubble Index", "Funding/Sentiment Integration", "Persistent Bubble History"]}
 
 
 
 
 
425
 
426
  @app.get("/consilium")
427
  async def consilium(symbol: str = Query("XAU/USD")):
428
  if symbol not in SYMBOLS:
429
+ return {"error": "Invalid symbol"}
430
+ return await get_regime_signal(symbol)
431
 
432
  @app.get("/regime/{symbol}")
433
  async def regime(symbol: str):
434
  if symbol not in SYMBOLS:
435
+ return {"error": "Invalid symbol"}
436
+ return await detect_market_regime(symbol)
 
 
 
437
 
438
  @app.get("/euphoria/{symbol}")
439
  async def euphoria(symbol: str):
440
  if symbol not in SYMBOLS:
441
+ return {"error": "Invalid symbol"}
442
+ df = await fetch_candles(symbol)
443
  if df is None:
444
+ return {"error": "no data"}
445
+ deriv = await fetch_derivatives(symbol) if symbol != "XAU/USD" else None
446
+ sent = await fetch_sentiment_signal(symbol)
447
+ fear = None
448
+ if sent:
449
+ reddit = sent.get("metrics", {}).get("reddit", {})
450
+ fear = reddit.get("fear_ratio")
451
+ funding_signal = deriv.get("funding_rate", {}).get("signal") if deriv else None
452
+ oi_change = deriv.get("open_interest", {}).get("change_pct") if deriv else None
453
+ ei = calculate_euphoria_index(df, funding_signal, oi_change, fear)
454
+ return {"symbol": symbol, "euphoria_index": round(ei, 1)}
455
 
456
  @app.get("/nvt/{symbol}")
457
  async def nvt(symbol: str):
458
  if symbol not in ["ETH/USD", "SOL/USD"]:
459
  return {"error": "NVT доступен только для крипты"}
460
+ df = await fetch_candles(symbol)
461
  if df is None:
462
+ return {"error": "no data"}
463
+ onchain_vol = await fetch_onchain_volume(symbol)
464
+ nvt_score = calculate_nvt_ratio(symbol, df, onchain_vol)
465
+ return {"symbol": symbol, "nvt_score": round(nvt_score, 1), "onchain_volume_used": onchain_vol is not None}
 
466
 
467
+ if __name__ == "__main__":
468
+ import uvicorn
469
+ uvicorn.run(app, host="0.0.0.0", port=7860)
 
 
 
 
 
 
 
470
 
471
+ print("🚀 SPACE 28 v2.0 — MARKET REGIME & BUBBLE SENTINEL ЗАПУЩЕН!")