tomirisg25 commited on
Commit
4ec1dc1
·
verified ·
1 Parent(s): 63ca10d

Update app.py

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