tomirisai80 commited on
Commit
ebef207
·
verified ·
1 Parent(s): ec683a0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +552 -255
app.py CHANGED
@@ -1,33 +1,22 @@
1
  # ============================================
2
- # АВТО-УСТАНОВКА ПАКЕТОВ
3
  # ============================================
4
- import subprocess, sys, importlib
5
-
6
- REQUIRED_PACKAGES = {
7
- 'numpy': 'numpy',
8
- 'pandas': 'pandas',
9
- 'requests': 'requests'
10
- }
11
-
12
- for module_name, pip_name in REQUIRED_PACKAGES.items():
13
- try:
14
- importlib.import_module(module_name)
15
- except ImportError:
16
- print(f"📦 Устанавливаю {pip_name}...")
17
- subprocess.check_call([sys.executable, "-m", "pip", "install", pip_name])
18
- print(f"✅ {pip_name} установлен!")
19
-
20
- # ============================================
21
- # 👑 TOMIRIS SPACE 19 v1.2 — SOL/USD MASTER (ансамбль daily+4h)
22
  # ============================================
23
 
24
- import os, time, json, requests, threading
25
  from typing import Dict, Any, Optional, List, Tuple
26
  import numpy as np, pandas as pd
 
27
  from datetime import datetime
28
  from collections import deque
29
  from fastapi import FastAPI, Query
30
- import warnings
31
  warnings.filterwarnings('ignore')
32
 
33
  # ================= БЕЗОПАСНЫЙ ИМПОРТ =================
@@ -39,21 +28,22 @@ try:
39
  import joblib
40
  HAS_JOBLIB = True
41
  except:
42
- pass
43
 
44
  try:
45
  import firebase_admin
46
  from firebase_admin import credentials, firestore
47
  HAS_FIREBASE = True
48
  except:
49
- pass
50
 
51
  try:
52
  import yfinance as yf
53
  HAS_YFINANCE = True
54
  except:
55
- pass
56
 
 
57
  db = None
58
  if HAS_FIREBASE:
59
  try:
@@ -61,53 +51,75 @@ if HAS_FIREBASE:
61
  firebase_admin.initialize_app(cred)
62
  db = firestore.client()
63
  print("✅ Firebase подключен")
64
- except:
65
- pass
66
 
67
- SPACE_URLS = {
 
68
  "space_17_hub": "https://tomiris-ai-name5-5.hf.space",
69
- "space_18_arbiter": "https://tomiris-ai-name6-6.hf.space"
 
70
  }
71
 
72
- TWELVE_KEYS = [
 
73
  "e3740c072fda4fe8b8539d40b07e445e",
74
  "58e67e0008e24161ac9b1671b7c2d2d0"
75
  ]
76
 
77
- SYMBOL = "SOL/USD"
78
- TIMEFRAMES = ["15min", "1h", "4h"]
 
 
79
 
80
  try:
81
  with open("best_config.json", "r") as f:
82
  config = json.load(f)
83
- SOL_THRESHOLD = config.get("sol", {}).get("threshold", 0.52)
84
- TRADING_RULES = config.get("trading_rules", {})
85
  except:
86
- SOL_THRESHOLD = 0.52
87
- TRADING_RULES = {}
 
 
 
 
 
 
88
 
89
- CACHE_TTL = 900
90
- MT5_MAX_AGE_SEC = 300
91
- HUB_CACHE_TTL = 5.0
92
 
93
- REGIME_WEIGHTS = {
 
94
  "TREND": {"model": 0.70, "tf": 0.30},
95
  "VOLATILE": {"model": 0.50, "tf": 0.50},
96
  "CONGESTED": {"model": 0.45, "tf": 0.55},
97
  "RANGE": {"model": 0.60, "tf": 0.40}
98
  }
99
 
100
- FEATURES_STORE = {}
101
- DATA_CACHE = {}
 
102
  PREDICTION_HISTORY = deque(maxlen=500)
103
- HUB_CACHE = {"price": 0.0, "timestamp": 0.0, "fresh": False}
104
- CIRCUIT_BREAKERS = {}
105
- LAST_CONFIDENCE = 0.5
 
 
 
 
 
 
 
 
106
 
107
- YAHOO_INTERVAL_MAP = {"15min": "15m", "1h": "60m", "4h": "4h"}
108
 
109
- print(f"🔥 SPACE 19 v1.2: Загрузка моделей для {SYMBOL}...")
110
- MODELS = {"xgb_daily": None, "xgb_4h": None, "lgb": None}
 
111
 
112
  if HAS_JOBLIB:
113
  try:
@@ -115,24 +127,22 @@ if HAS_JOBLIB:
115
  print("✅ XGBoost SOL daily загружен")
116
  except Exception as e:
117
  print(f"⚠️ XGBoost SOL daily: {e}")
118
-
119
  try:
120
  MODELS["xgb_4h"] = joblib.load("xgboost_sol_4h.joblib")
121
  print("✅ XGBoost SOL 4h загружен")
122
  except Exception as e:
123
  print(f"⚠️ XGBoost SOL 4h: {e}")
124
-
125
  try:
126
  MODELS["lgb"] = joblib.load("lgb_sol.joblib")
127
  print("✅ LightGBM SOL загружен")
128
  except:
129
  print("⚠️ LightGBM SOL не найден")
130
 
131
- # ================= ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ =================
132
  api_lock = threading.Lock()
133
- twelve_counter = 0
134
 
135
- def get_next_twelve_key():
136
  global twelve_counter
137
  with api_lock:
138
  key = TWELVE_KEYS[twelve_counter % len(TWELVE_KEYS)]
@@ -140,19 +150,20 @@ def get_next_twelve_key():
140
  return key
141
 
142
  session = requests.Session()
143
- session.headers.update({"User-Agent": "Tomiris-Space19"})
144
 
145
- def safe_float(value, default=0.0):
146
  try:
147
  if isinstance(value, (pd.Series, pd.DataFrame)):
148
  val = value.iloc[-1] if len(value) > 0 else default
149
  else:
150
  val = value
151
- return float(val) if not pd.isna(val) else default
 
152
  except:
153
  return default
154
 
155
- def safe_rsi(close_series, period=14):
156
  try:
157
  delta = close_series.diff()
158
  gain = delta.clip(lower=0).rolling(period, min_periods=period).mean()
@@ -165,7 +176,7 @@ def safe_rsi(close_series, period=14):
165
  except:
166
  return 50.0
167
 
168
- def safe_ema(close_series, span):
169
  try:
170
  ema = close_series.ewm(span=span, adjust=False).mean()
171
  return ema, safe_float(ema.iloc[-1])
@@ -173,32 +184,38 @@ def safe_ema(close_series, span):
173
  return None, 0.0
174
 
175
  class KalmanFilter:
176
- def __init__(self, process_noise=1e-5, measurement_noise=1e-4):
177
- self.q = process_noise; self.r = measurement_noise
178
- self.x = 0.0; self.p = 1.0
179
- def update(self, z):
 
 
180
  self.p += self.q
181
  k = self.p / (self.p + self.r)
182
  self.x += k * (z - self.x)
183
  self.p *= (1 - k)
184
  return self.x
185
 
186
- def hurst_exponent(series, lags=20):
187
- if len(series) < lags * 2: return 0.5
 
188
  lags_range = range(2, min(lags, len(series)//2))
189
  tau = [np.std(np.subtract(series.values[lag:], series.values[:-lag])) for lag in lags_range]
190
  try:
191
  poly = np.polyfit(np.log(list(lags_range)), np.log(tau), 1)
192
  return float(poly[0] * 2.0)
193
- except: return 0.5
 
194
 
195
- def fetch_google_trends_index(keyword):
196
  return 50.0
197
 
198
- def get_mt5_price_from_hub():
 
199
  global HUB_CACHE
200
  if time.time() - HUB_CACHE.get("timestamp", 0) < HUB_CACHE_TTL:
201
- if HUB_CACHE.get("fresh"): return HUB_CACHE
 
202
  try:
203
  r = requests.get(f"{SPACE_URLS['space_17_hub']}/price/{SYMBOL}", timeout=3)
204
  if r.status_code == 200:
@@ -206,14 +223,18 @@ def get_mt5_price_from_hub():
206
  fresh = data.get("fresh", False)
207
  mid = data.get("mid", 0)
208
  if fresh and mid > 0:
209
- HUB_CACHE = {"price": mid, "bid": data.get("bid",0), "ask": data.get("ask",0),
210
- "spread_pct": data.get("spread_pct",0), "timestamp": time.time(),
211
- "fresh": True, "source": "MT5_LIVE"}
 
 
212
  return HUB_CACHE
213
- except: pass
 
214
  return {"price":0.0, "timestamp":time.time(), "fresh":False, "source":"UNAVAILABLE"}
215
 
216
- def breaker_open(name):
 
217
  info = CIRCUIT_BREAKERS.get(name)
218
  if not info: return False
219
  if info["fails"] < 5: return False
@@ -222,37 +243,44 @@ def breaker_open(name):
222
  return False
223
  return True
224
 
225
- def breaker_fail(name):
226
  info = CIRCUIT_BREAKERS.get(name, {"fails":0, "last_fail":0})
227
- info["fails"] += 1; info["last_fail"] = time.time()
 
228
  CIRCUIT_BREAKERS[name] = info
229
 
230
- def smooth_confidence(current):
 
231
  global LAST_CONFIDENCE
232
  current = max(0.0, min(1.0, current))
233
  smoothed = LAST_CONFIDENCE * 0.7 + current * 0.3
234
  LAST_CONFIDENCE = smoothed
235
  return smoothed
236
 
237
- def detect_market_regime(features):
238
  adx = features.get("adx", 20.0)
239
  volatility = features.get("volatility_1h", 0.0)
240
  hurst = features.get("hurst_exponent", 0.5)
241
- if adx > 30 and hurst > 0.55: return "TREND"
242
- if volatility > 0.04: return "VOLATILE"
 
 
243
  return "RANGE"
244
 
245
- def stress_test(features):
246
- if features.get("atr_pct", 3.0) > 12.0: return "WAIT"
247
- if features.get("volatility_1h", 0.0) > 0.08: return "WAIT"
 
 
248
  return None
249
 
250
  # ================= ЗАГРУЗКА ДАННЫХ =================
251
- def fetch_twelvedata_sol(tf="1h"):
252
  cache_key = f"td_sol_{tf}"
253
  if cache_key in DATA_CACHE:
254
  age = time.time() - DATA_CACHE[cache_key].get("timestamp",0)
255
- if age < CACHE_TTL: return DATA_CACHE[cache_key]["df"], DATA_CACHE[cache_key]["source"]
 
256
  for key_idx, key in enumerate(TWELVE_KEYS):
257
  try:
258
  url = f"https://api.twelvedata.com/time_series?symbol=SOL/USD&interval={tf}&outputsize=200&apikey={key}"
@@ -261,136 +289,197 @@ def fetch_twelvedata_sol(tf="1h"):
261
  data = r.json()
262
  if "values" in data:
263
  df = pd.DataFrame(data["values"]).iloc[::-1].reset_index(drop=True)
264
- for col in ["close","high","low","open"]: df[col] = pd.to_numeric(df[col], errors="coerce")
 
265
  df["volume"] = pd.to_numeric(df.get("volume",0), errors="coerce").fillna(0)
266
  df = df.dropna(subset=["close","high","low","open"])
267
  if len(df) >= 30:
268
- DATA_CACHE[cache_key] = {"df":df, "source":f"TwelveData-Key{key_idx+1}", "timestamp":time.time()}
 
 
 
 
269
  return df, f"TwelveData-Key{key_idx+1}"
270
- elif r.status_code == 429: continue
271
- except: continue
 
 
272
  if HAS_YFINANCE:
273
  try:
274
  yf_interval = YAHOO_INTERVAL_MAP.get(tf, "60m")
275
  period_map = {"15min":"7d", "1h":"60d", "4h":"60d"}
276
  yf_data = yf.download("SOL-USD", period=period_map.get(tf,"60d"), interval=yf_interval, progress=False)
277
  if not yf_data.empty:
278
- df = pd.DataFrame({'close': yf_data['Close'].values.flatten(),
279
- 'high': yf_data['High'].values.flatten(),
280
- 'low': yf_data['Low'].values.flatten(),
281
- 'open': yf_data['Open'].values.flatten(),
282
- 'volume': yf_data['Volume'].values.flatten()}).dropna()
 
 
283
  if len(df) >= 30:
284
  DATA_CACHE[cache_key] = {"df":df, "source":"YahooFinance", "timestamp":time.time()}
285
  return df, "YahooFinance"
286
- except: pass
 
287
  return None, None
288
 
289
- # Ончейн-функции
290
- def fetch_solana_onchain():
291
  cache_key = "solana_onchain"
292
  if cache_key in DATA_CACHE and time.time() - DATA_CACHE[cache_key].get("timestamp",0) < 300:
293
  return DATA_CACHE[cache_key]["data"]
294
  result = {}
 
295
  try:
296
- r = requests.get("https://api.llama.fi/v2/tvl/solana", timeout=10)
297
  if r.status_code == 200:
298
  data = r.json()
299
  result['tvl'] = data.get('tvl',0)
300
  result['tvl_change_24h'] = data.get('change_1d',0)
301
  result['tvl_trend'] = 'UP' if data.get('change_1d',0) > 0 else 'DOWN'
302
- except: pass
 
 
303
  try:
304
- r = requests.get("https://api.llama.fi/overview/dexs/solana?excludeTotalDataChart=true&excludeTotalDataChartBreakdown=true", timeout=10)
305
  if r.status_code == 200:
306
  data = r.json()
307
  result['dex_volume_24h'] = data.get('total24h',0)
308
  result['dex_change_24h'] = data.get('change_1d',0)
309
- except: pass
310
- try:
311
- r = requests.get("https://api.llama.fi/overview/Solana?excludeTotalDataChart=true&excludeTotalDataChartBreakdown=true", timeout=10)
312
- if r.status_code == 200:
313
- data = r.json()
314
- result['active_users'] = data.get('activeUsers',0)
315
- except: pass
316
  DATA_CACHE[cache_key] = {"data": result, "timestamp": time.time()}
317
  return result
318
 
319
- def fetch_pump_fun_activity():
 
320
  try:
321
- r = requests.get("https://api.coingecko.com/api/v3/coins/solana?community_data=true&developer_data=true", timeout=10)
322
  if r.status_code == 200:
323
- data = r.json()
324
- comm = data.get('community_data', {})
325
- dev = data.get('developer_data', {})
326
- return {'reddit_activity': comm.get('reddit_average_posts_48h',0),
327
- 'developer_score': dev.get('developer_score',0),
328
- 'ecosystem_activity': 'HIGH' if dev.get('developer_score',0) > 80 else 'MODERATE'}
329
- except: pass
330
- return {'ecosystem_activity': 'MODERATE'}
331
-
332
- def fetch_coingecko_sol():
333
- try:
334
- r = session.get("https://api.coingecko.com/api/v3/coins/solana?localization=false&tickers=false&community_data=false&developer_data=false", timeout=10)
335
- if r.status_code == 200:
336
- md = r.json().get('market_data', {})
337
- return {'market_cap': md.get('market_cap',{}).get('usd',0),
338
- 'total_volume': md.get('total_volume',{}).get('usd',0),
339
- 'price_change_24h': md.get('price_change_percentage_24h',0)}
340
- except: pass
341
- return {'market_cap':0, 'total_volume':0, 'price_change_24h':0}
342
 
343
- def fetch_binance_sol():
 
344
  result = {}
345
  try:
346
- r = requests.get("https://fapi.binance.com/fapi/v1/premiumIndex", timeout=10)
347
  if r.status_code == 200:
348
  for item in r.json():
349
  if item.get('symbol') == 'SOLUSDT':
350
  fr = float(item.get('lastFundingRate',0))
351
  result['funding_rate'] = fr
352
  result['funding_signal'] = 'BEARISH' if fr > 0.001 else 'BULLISH' if fr < -0.001 else 'NEUTRAL'
353
- except: pass
 
 
354
  try:
355
- r = requests.get("https://fapi.binance.com/fapi/v1/openInterest?symbol=SOLUSDT", timeout=10)
356
- if r.status_code == 200: result['open_interest'] = float(r.json().get('openInterest',0))
357
- except: pass
 
 
358
  return result
359
 
360
- def build_features_from_mt5(mt5_features):
361
- features = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
  for k, v in mt5_features.items():
363
- if isinstance(v, (int, float, np.floating, np.integer)): features[k] = float(v)
364
- elif isinstance(v, np.bool_): features[k] = bool(v)
365
- else: features[k] = v
366
- while len(features) < 200: features[f"mt5_pad_{len(features)}"] = 0.0
 
 
 
 
367
  return features
368
 
369
- def build_sol_features(df, onchain_data=None):
370
- if df is None or len(df) < 20: return {}
 
 
 
 
371
  try:
372
- close = df["close"].astype(float); high = df["high"].astype(float)
373
- low = df["low"].astype(float); open_p = df["open"].astype(float) if "open" in df.columns else close
 
 
374
  volume = df["volume"].astype(float) if "volume" in df.columns else pd.Series([0.0]*len(df))
375
- except: return {}
376
- features = {}
 
 
377
  features["price"] = safe_float(close.iloc[-1])
378
  features["return_1h"] = safe_float(close.pct_change(1).iloc[-1])
379
- features["return_24h"] = safe_float(close.pct_change(24).iloc[-1]) if len(close)>24 else 0.0
 
380
  kf = KalmanFilter()
381
  kalman_close = [kf.update(x) for x in close.values]
382
  features["kalman_price"] = kalman_close[-1]
383
  features["kalman_diff"] = close.iloc[-1] - kalman_close[-1]
 
384
  features["hurst_exponent"] = hurst_exponent(close)
 
385
  ret = close.pct_change()
386
- features["volatility_1h"] = safe_float(ret.rolling(24, min_periods=24).std().iloc[-1]) if len(close)>=24 else 0.0
387
- features["high_low_ratio"] = safe_float(((high.iloc[-1]-low.iloc[-1])/(close.iloc[-1]+1e-10))*100)
388
- for span in [9,21,50]:
 
389
  if len(close) >= span:
390
  _, ema_val = safe_ema(close, span)
391
  if ema_val != 0:
392
  features[f"ema_{span}"] = ema_val
393
- features[f"price_vs_ema_{span}"] = safe_float(((close.iloc[-1]-ema_val)/ema_val)*100)
 
394
  if len(close) >= 26:
395
  try:
396
  ema12 = close.ewm(span=12, adjust=False).mean()
@@ -400,210 +489,418 @@ def build_sol_features(df, onchain_data=None):
400
  features["macd"] = safe_float(macd.iloc[-1])
401
  features["macd_signal"] = safe_float(signal.iloc[-1])
402
  features["macd_hist"] = features["macd"] - features["macd_signal"]
403
- except: pass
404
- features["rsi_14"] = safe_rsi(close, 14) if len(close)>=14 else 50.0
 
 
 
405
  if len(close) >= 14:
406
  try:
407
  prev_close = close.shift(1)
408
- tr = pd.DataFrame({"tr1":high-low, "tr2":(high-prev_close).abs(), "tr3":(low-prev_close).abs()}).max(axis=1)
 
 
 
 
409
  features["atr_14"] = safe_float(tr.rolling(14, min_periods=14).mean().iloc[-1])
410
- features["atr_pct"] = (features["atr_14"]/(close.iloc[-1]+1e-10))*100
411
- except: features["atr_14"] = close.iloc[-1]*0.02
 
 
 
412
  if onchain_data:
413
- features["tvl"] = onchain_data.get("tvl",0)
414
- features["tvl_trend"] = 1 if onchain_data.get("tvl_trend")=="UP" else -1
415
- features["dex_volume_24h"] = onchain_data.get("dex_volume_24h",0)
416
- features["active_users"] = onchain_data.get("active_users",0)
417
- for kw in ["solana","memecoin","pump_fun","firedancer"]: features[f"trends_{kw}"] = fetch_google_trends_index(kw)
 
 
 
 
 
 
 
 
 
 
 
418
  now = datetime.utcnow()
419
- features["is_weekend"] = 1 if now.weekday()>=5 else 0
420
  features["hour"] = now.hour
421
- while len(features) < 200: features[f"pad_{len(features)}"] = 0.0
 
 
 
422
  return features
423
 
424
- def get_multi_tf_features(onchain_data):
425
- all_features, sources = {}, []
 
 
426
  for tf in TIMEFRAMES:
427
  df, source = fetch_twelvedata_sol(tf)
428
  if df is not None and len(df) >= 30:
429
- feats = build_sol_features(df, onchain_data)
430
- if feats: all_features[tf] = feats; sources.append(source or "Unknown")
 
 
431
  return all_features, sources
432
 
433
- # ================= ГЛАВНЫЙ СИГНАЛ (ансамбль daily + 4h) =================
434
- def get_sol_signal():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
435
  global LAST_CONFIDENCE
436
  start = time.time()
437
- mt5_features = None; mt5_price = None; data_source = "UNKNOWN"
 
 
 
 
 
 
 
 
 
 
 
438
  fs = FEATURES_STORE.get(SYMBOL, {})
439
  age = time.time() - fs.get("timestamp", 0)
440
  if age < MT5_MAX_AGE_SEC:
441
- mt5_features = fs.get("features", {}); mt5_price = fs.get("price"); data_source = "MT5"
442
- onchain_data = fetch_solana_onchain()
443
- pump_fun = fetch_pump_fun_activity()
444
- coingecko = fetch_coingecko_sol()
445
- binance = fetch_binance_sol()
446
- mtf_features, sources = {}, []
 
 
447
  if mt5_features and len(mt5_features) >= 50:
448
  model_features = build_features_from_mt5(mt5_features)
449
- model_features["tvl"] = onchain_data.get("tvl",0)
450
- model_features["tvl_trend"] = 1 if onchain_data.get("tvl_trend")=="UP" else -1
451
- model_features["active_users"] = onchain_data.get("active_users",0)
452
- price = mt5_price or model_features.get("H1_price", model_features.get("price",0))
 
 
 
 
 
 
453
  sources = ["MT5"]
454
  else:
455
- mtf_features, sources = get_multi_tf_features(onchain_data)
456
- if not mtf_features: return None
 
 
 
457
  h1_features = mtf_features.get("1h", list(mtf_features.values())[0])
458
  model_features = h1_features
459
- price = h1_features.get("price",0)
460
  data_source = "+".join(sources) if sources else "API"
461
- if price == 0: return None
 
 
 
 
462
  stress = stress_test(model_features)
463
  if stress == "WAIT":
464
- return {"space":"space_19_sol_master","symbol":SYMBOL,"signal":{"direction":"WAIT","confidence":0.0},"reason":"stress_test_black_swan"}
 
 
 
 
 
 
 
465
  regime = detect_market_regime(model_features)
466
-
467
- # ----- АНСАМБЛЬ DAILY + 4H -----
 
468
  xgb_prob = 0.5
 
469
  if model_features:
470
  try:
471
  fv = list(model_features.values())[:200]
472
  while len(fv) < 200:
473
  fv.append(0.0)
474
  X = np.nan_to_num(np.array(fv, dtype=np.float64).reshape(1, -1))
 
475
  probs = []
476
  if MODELS.get("xgb_daily"):
477
  proba = MODELS["xgb_daily"].predict_proba(X)[0]
478
  probs.append(float(proba[1] if len(proba) > 1 else proba[0]))
 
479
  if MODELS.get("xgb_4h"):
480
  proba = MODELS["xgb_4h"].predict_proba(X)[0]
481
  probs.append(float(proba[1] if len(proba) > 1 else proba[0]))
482
- if probs:
 
 
 
 
 
 
 
 
 
483
  xgb_prob = sum(probs) / len(probs)
484
- xgb_prob = max(0.0, min(1.0, xgb_prob))
485
  except Exception as e:
486
  print(f" ⚠️ Ошибка предсказания: {e}")
487
 
488
- onchain_score = 0.0
489
- if onchain_data.get("tvl_trend")=="UP": onchain_score += 0.1
490
- if onchain_data.get("dex_change_24h",0) > 10: onchain_score += 0.05
491
- if pump_fun.get("ecosystem_activity")=="HIGH": onchain_score += 0.05
492
- if binance.get("funding_signal")=="BULLISH": onchain_score += 0.05
493
- elif binance.get("funding_signal")=="BEARISH": onchain_score -= 0.05
494
-
495
  if data_source == "MT5":
496
- confirmations, total_tf = 0,0
497
- for tf_key in ["M15","H1","H4"]:
498
  ema_key = f"{tf_key}_price_vs_ema_21"
499
  if ema_key in model_features:
500
  total_tf += 1
501
- if model_features.get(ema_key,0) > 0: confirmations += 1
502
- else: confirmations -= 1
503
- tf_score = confirmations / max(total_tf,1)
504
- tf_norm = (tf_score + 1)/2
 
 
505
  else:
506
- confirmations, total_tf = 0,0
507
- for tf_key in ["15min","1h","4h"]:
508
  tf_feats = mtf_features.get(tf_key, {})
509
- if not tf_feats: continue
 
510
  total_tf += 1
511
- ema_score = tf_feats.get("price_vs_ema_21",0)
512
- rsi_val = tf_feats.get("rsi_14",50)
513
- macd_hist = tf_feats.get("macd_hist",0)
514
- if ema_score > 0 and rsi_val > 50 and macd_hist > 0: confirmations += 1
515
- elif ema_score < 0 and rsi_val < 50 and macd_hist < 0: confirmations -= 1
516
- tf_score = confirmations / max(total_tf,1)
517
- tf_norm = (tf_score + 1)/2
518
-
519
- w = REGIME_WEIGHTS.get(regime, REGIME_WEIGHTS["RANGE"])
520
- final_score = xgb_prob * w["model"] * 0.50 + tf_norm * w["tf"] * 0.20 + (0.5 + onchain_score) * 0.30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
521
  confidence = smooth_confidence(final_score)
522
 
523
- if confidence > SOL_THRESHOLD + 0.08: direction = "LONG"
524
- elif confidence < SOL_THRESHOLD - 0.08: direction = "SHORT"
525
- else: direction = "WAIT"
 
 
 
526
 
527
- atr = model_features.get("atr_14", price*0.02)
 
528
  sl_mult = TRADING_RULES.get("sl_atr_multiplier", 2.0)
529
  tp_mult = TRADING_RULES.get("tp_atr_multiplier", 4.0)
530
- sl_dist = atr * sl_mult; tp_dist = atr * tp_mult
531
- sl = round(price - sl_dist, 2) if direction == "LONG" else round(price + sl_dist, 2) if direction == "SHORT" else 0
532
- tp = round(price + tp_dist, 2) if direction == "LONG" else round(price - tp_dist, 2) if direction == "SHORT" else 0
 
 
 
 
 
 
 
533
 
534
- PREDICTION_HISTORY.append({"timestamp": datetime.utcnow().isoformat(), "direction": direction, "confidence": confidence, "price": price})
535
- if db:
536
- try: db.collection("space19_sol_signals").add({"direction":direction,"confidence":confidence,"price":price,"regime":regime,"data_source":data_source,"timestamp": firestore.SERVER_TIMESTAMP})
537
- except: pass
 
 
538
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
539
  result = {
540
- "space": "space_19_sol_master", "timestamp": int(time.time()), "symbol": SYMBOL,
541
- "signal": {"direction": direction, "confidence": round(confidence, 4)},
 
 
 
 
 
 
542
  "analysis": {
543
- "xgb_probability": round(xgb_prob,4),
544
- "models_used": sum(1 for m in ["xgb_daily","xgb_4h"] if MODELS.get(m) is not None),
545
- "multi_tf_score": round(tf_score,4),
546
- "onchain_score": round(onchain_score,4),
547
  "market_regime": regime,
548
  "data_source": data_source
549
  },
550
- "onchain": {"tvl": onchain_data.get("tvl",0), "tvl_trend": onchain_data.get("tvl_trend","STABLE"), "active_users": onchain_data.get("active_users",0), "pump_fun": pump_fun, "binance": binance},
551
- "risk": {"sl": sl, "tp": tp, "rr": round(tp_dist/(sl_dist+1e-10),2) if direction!="WAIT" else 0},
552
- "meta": {"latency_ms": int((time.time()-start)*1000), "model_version": "v1.2 ensemble", "features_used": len(model_features) if model_features else 0}
 
 
 
 
 
 
 
 
 
 
 
553
  }
554
- try:
555
- requests.post(f"{SPACE_URLS['space_18_arbiter']}/log_signal", json={"space":"space_19_sol","symbol":SYMBOL,"signal":result.get("signal",{})}, timeout=5)
556
- except: pass
557
- print(f"🥉 SOL/USD: {direction} | conf={confidence:.3f} | ensemble={xgb_prob:.3f} | models={result['analysis']['models_used']} | regime={regime}")
558
  return result
559
 
 
560
  def keep_alive():
561
  while True:
562
  time.sleep(840)
563
- try: requests.get("http://localhost:7860/health", timeout=5)
564
- except: pass
 
 
565
  threading.Thread(target=keep_alive, daemon=True).start()
566
 
567
- app = FastAPI(title="TOMIRIS SPACE 19 v1.2 — SOL/USD MASTER (Ensemble)")
 
568
 
569
  @app.get("/health")
570
- def health():
571
- models_loaded = sum(1 for m in ["xgb_daily","xgb_4h"] if MODELS.get(m) is not None)
572
  return {
573
- "space": "Space 19 v1.2 Ensemble",
574
  "status": "operational",
575
  "symbol": SYMBOL,
576
  "models_loaded": models_loaded,
577
- "models": {"daily": MODELS["xgb_daily"] is not None, "4h": MODELS["xgb_4h"] is not None}
 
578
  }
579
 
580
  @app.get("/consilium")
581
- def consilium():
582
  try:
583
  signal = get_sol_signal()
584
- return signal if signal else {"space":"space_19_sol_master","symbol":SYMBOL,"signal":{"direction":"WAIT","confidence":0.0},"error":"no_data"}
 
 
585
  except Exception as e:
586
  return {"space":"space_19_sol_master","symbol":SYMBOL,"signal":{"direction":"WAIT","confidence":0.0},"error": str(e)[:100]}
587
 
588
  @app.get("/signal")
589
- def signal(): return consilium()
 
590
 
591
  @app.get("/onchain")
592
- def onchain():
593
- return {"solana": fetch_solana_onchain(), "pump_fun": fetch_pump_fun_activity(), "coingecko": fetch_coingecko_sol(), "binance": fetch_binance_sol()}
 
 
 
 
 
594
 
595
  @app.get("/price")
596
- def current_price():
597
  hub = get_mt5_price_from_hub()
598
  return {"symbol": SYMBOL, "price": hub["price"] if hub["fresh"] else HUB_CACHE.get("price",0.0), "source": hub["source"], "fresh": hub["fresh"]}
599
 
600
  @app.post("/features")
601
- def receive_features(data: Dict[str, Any]):
602
- symbol = data.get("symbol", "SOL/USD")
603
- FEATURES_STORE[symbol] = {"features": data.get("features",{}), "price": data.get("price",0.0), "timestamp": time.time()}
 
 
 
 
604
  print(f"📥 MT5 {symbol}: {len(data.get('features',{}))} признаков")
605
  return {"status": "ok"}
606
 
607
- print("🚀 SPACE 19 v1.2 — SOL/USD MASTER ENSEMBLE ЗАПУЩЕН!")
608
- print("🥉 Обе модели (daily + 4h) работают вместе!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
609
  print("✅ Готов к бою!")
 
1
  # ============================================
2
+ # 👑 TOMIRIS SPACE 19 v8.0 — SOL/USD MASTER (Solana Dominance)
3
  # ============================================
4
+ # Улучшения относительно v1.2:
5
+ # 🔥 Meta SOL Score: TVL + DEX Volume + Active Addresses + Developer Activity
6
+ # ✅ Динамические веса компонентов (Accuracy × Sharpe × RegimeScore)
7
+ # ✅ Байесовское сглаживание confidence
8
+ # ✅ Интеграция с Space 31 (Performance Engine)
9
+ # ✅ Эндпоинт /explain для интерпретации
10
+ # ✅ Все старые функции (Kalman, Hurst, ensemble daily+4h) сохранены
 
 
 
 
 
 
 
 
 
 
 
11
  # ============================================
12
 
13
+ import os, time, threading, warnings, json, asyncio
14
  from typing import Dict, Any, Optional, List, Tuple
15
  import numpy as np, pandas as pd
16
+ import requests
17
  from datetime import datetime
18
  from collections import deque
19
  from fastapi import FastAPI, Query
 
20
  warnings.filterwarnings('ignore')
21
 
22
  # ================= БЕЗОПАСНЫЙ ИМПОРТ =================
 
28
  import joblib
29
  HAS_JOBLIB = True
30
  except:
31
+ print("⚠️ joblib не установлен")
32
 
33
  try:
34
  import firebase_admin
35
  from firebase_admin import credentials, firestore
36
  HAS_FIREBASE = True
37
  except:
38
+ print("⚠️ firebase_admin не установлен")
39
 
40
  try:
41
  import yfinance as yf
42
  HAS_YFINANCE = True
43
  except:
44
+ print("⚠️ yfinance не установлен")
45
 
46
+ # ================= FIREBASE =================
47
  db = None
48
  if HAS_FIREBASE:
49
  try:
 
51
  firebase_admin.initialize_app(cred)
52
  db = firestore.client()
53
  print("✅ Firebase подключен")
54
+ except Exception as e:
55
+ print(f"⚠️ Firebase: {e}")
56
 
57
+ # ================= URL'ы СМЕЖНЫХ SPACE'ов =================
58
+ SPACE_URLS: Dict[str, str] = {
59
  "space_17_hub": "https://tomiris-ai-name5-5.hf.space",
60
+ "space_18_arbiter": "https://tomiris-ai-name6-6.hf.space",
61
+ "space_31_perf": "https://nuxotetotmailsvoboden-tomiris-perf.hf.space" # если есть
62
  }
63
 
64
+ # ================= API КЛЮЧИ =================
65
+ TWELVE_KEYS: List[str] = [
66
  "e3740c072fda4fe8b8539d40b07e445e",
67
  "58e67e0008e24161ac9b1671b7c2d2d0"
68
  ]
69
 
70
+ # ================= КОНФИГУРАЦИЯ =================
71
+ SYMBOL: str = "SOL/USD"
72
+ MT5_SYMBOL: str = "SOLUSD"
73
+ TIMEFRAMES: List[str] = ["15min", "1h", "4h"]
74
 
75
  try:
76
  with open("best_config.json", "r") as f:
77
  config = json.load(f)
78
+ SOL_THRESHOLD: float = config.get("sol", {}).get("threshold", 0.52)
79
+ TRADING_RULES: Dict[str, Any] = config.get("trading_rules", {})
80
  except:
81
+ SOL_THRESHOLD: float = 0.52
82
+ TRADING_RULES: Dict[str, Any] = {
83
+ "sl_atr_multiplier": 2.0,
84
+ "tp_atr_multiplier": 4.0,
85
+ "trailing_stop_activation": 0.005,
86
+ "trailing_stop_distance": 0.003,
87
+ "breakeven_at": 0.005
88
+ }
89
 
90
+ CACHE_TTL: int = 900
91
+ MT5_MAX_AGE_SEC: int = 300
92
+ HUB_CACHE_TTL: float = 5.0
93
 
94
+ # Адаптивные веса (начальные)
95
+ REGIME_WEIGHTS: Dict[str, Dict[str, float]] = {
96
  "TREND": {"model": 0.70, "tf": 0.30},
97
  "VOLATILE": {"model": 0.50, "tf": 0.50},
98
  "CONGESTED": {"model": 0.45, "tf": 0.55},
99
  "RANGE": {"model": 0.60, "tf": 0.40}
100
  }
101
 
102
+ # Хранилища
103
+ FEATURES_STORE: Dict[str, Any] = {}
104
+ DATA_CACHE: Dict[str, Dict[str, Any]] = {}
105
  PREDICTION_HISTORY = deque(maxlen=500)
106
+ HUB_CACHE: Dict[str, Any] = {"price": 0.0, "timestamp": 0.0, "fresh": False}
107
+ CIRCUIT_BREAKERS: Dict[str, Dict[str, int]] = {}
108
+ LAST_CONFIDENCE: float = 0.5
109
+
110
+ # История точности компонентов
111
+ COMPONENT_PERF: Dict[str, Dict[str, float]] = {
112
+ "model": {"correct": 0, "total": 1, "sharpe": 1.0},
113
+ "tf": {"correct": 0, "total": 1, "sharpe": 1.0},
114
+ "onchain": {"correct": 0, "total": 1, "sharpe": 1.0},
115
+ "derivatives": {"correct": 0, "total": 1, "sharpe": 1.0},
116
+ }
117
 
118
+ YAHOO_INTERVAL_MAP: Dict[str, str] = {"15min": "15m", "1h": "60m", "4h": "4h"}
119
 
120
+ # ================= ЗАГРУЗКА МОДЕЛЕЙ =================
121
+ print(f"🔥 SPACE 19 v8.0: Загрузка моделей для {SYMBOL}...")
122
+ MODELS: Dict[str, Optional[Any]] = {"xgb_daily": None, "xgb_4h": None, "lgb": None}
123
 
124
  if HAS_JOBLIB:
125
  try:
 
127
  print("✅ XGBoost SOL daily загружен")
128
  except Exception as e:
129
  print(f"⚠️ XGBoost SOL daily: {e}")
 
130
  try:
131
  MODELS["xgb_4h"] = joblib.load("xgboost_sol_4h.joblib")
132
  print("✅ XGBoost SOL 4h загружен")
133
  except Exception as e:
134
  print(f"⚠️ XGBoost SOL 4h: {e}")
 
135
  try:
136
  MODELS["lgb"] = joblib.load("lgb_sol.joblib")
137
  print("✅ LightGBM SOL загружен")
138
  except:
139
  print("⚠️ LightGBM SOL не найден")
140
 
141
+ # ================= УТИЛИТЫ =================
142
  api_lock = threading.Lock()
143
+ twelve_counter: int = 0
144
 
145
+ def get_next_twelve_key() -> str:
146
  global twelve_counter
147
  with api_lock:
148
  key = TWELVE_KEYS[twelve_counter % len(TWELVE_KEYS)]
 
150
  return key
151
 
152
  session = requests.Session()
153
+ session.headers.update({"User-Agent": "Tomiris-Space19-v8.0"})
154
 
155
+ def safe_float(value: Any, default: float = 0.0) -> float:
156
  try:
157
  if isinstance(value, (pd.Series, pd.DataFrame)):
158
  val = value.iloc[-1] if len(value) > 0 else default
159
  else:
160
  val = value
161
+ result = float(val)
162
+ return result if not pd.isna(result) else default
163
  except:
164
  return default
165
 
166
+ def safe_rsi(close_series: pd.Series, period: int = 14) -> float:
167
  try:
168
  delta = close_series.diff()
169
  gain = delta.clip(lower=0).rolling(period, min_periods=period).mean()
 
176
  except:
177
  return 50.0
178
 
179
+ def safe_ema(close_series: pd.Series, span: int) -> Tuple[Optional[pd.Series], float]:
180
  try:
181
  ema = close_series.ewm(span=span, adjust=False).mean()
182
  return ema, safe_float(ema.iloc[-1])
 
184
  return None, 0.0
185
 
186
  class KalmanFilter:
187
+ def __init__(self, process_noise: float = 1e-5, measurement_noise: float = 1e-4):
188
+ self.q = process_noise
189
+ self.r = measurement_noise
190
+ self.x = 0.0
191
+ self.p = 1.0
192
+ def update(self, z: float) -> float:
193
  self.p += self.q
194
  k = self.p / (self.p + self.r)
195
  self.x += k * (z - self.x)
196
  self.p *= (1 - k)
197
  return self.x
198
 
199
+ def hurst_exponent(series: pd.Series, lags: int = 20) -> float:
200
+ if len(series) < lags * 2:
201
+ return 0.5
202
  lags_range = range(2, min(lags, len(series)//2))
203
  tau = [np.std(np.subtract(series.values[lag:], series.values[:-lag])) for lag in lags_range]
204
  try:
205
  poly = np.polyfit(np.log(list(lags_range)), np.log(tau), 1)
206
  return float(poly[0] * 2.0)
207
+ except:
208
+ return 0.5
209
 
210
+ def fetch_google_trends_index(keyword: str) -> float:
211
  return 50.0
212
 
213
+ # ================= DATA HUB =================
214
+ def get_mt5_price_from_hub() -> Dict[str, Any]:
215
  global HUB_CACHE
216
  if time.time() - HUB_CACHE.get("timestamp", 0) < HUB_CACHE_TTL:
217
+ if HUB_CACHE.get("fresh"):
218
+ return HUB_CACHE
219
  try:
220
  r = requests.get(f"{SPACE_URLS['space_17_hub']}/price/{SYMBOL}", timeout=3)
221
  if r.status_code == 200:
 
223
  fresh = data.get("fresh", False)
224
  mid = data.get("mid", 0)
225
  if fresh and mid > 0:
226
+ HUB_CACHE = {
227
+ "price": mid, "bid": data.get("bid",0), "ask": data.get("ask",0),
228
+ "spread_pct": data.get("spread_pct",0), "timestamp": time.time(),
229
+ "fresh": True, "source": "MT5_LIVE"
230
+ }
231
  return HUB_CACHE
232
+ except:
233
+ pass
234
  return {"price":0.0, "timestamp":time.time(), "fresh":False, "source":"UNAVAILABLE"}
235
 
236
+ # ================= CIRCUIT BREAKER =================
237
+ def breaker_open(name: str) -> bool:
238
  info = CIRCUIT_BREAKERS.get(name)
239
  if not info: return False
240
  if info["fails"] < 5: return False
 
243
  return False
244
  return True
245
 
246
+ def breaker_fail(name: str) -> None:
247
  info = CIRCUIT_BREAKERS.get(name, {"fails":0, "last_fail":0})
248
+ info["fails"] += 1
249
+ info["last_fail"] = time.time()
250
  CIRCUIT_BREAKERS[name] = info
251
 
252
+ # ================= СГЛАЖИВАНИЕ УВЕРЕННОСТИ =================
253
+ def smooth_confidence(current: float) -> float:
254
  global LAST_CONFIDENCE
255
  current = max(0.0, min(1.0, current))
256
  smoothed = LAST_CONFIDENCE * 0.7 + current * 0.3
257
  LAST_CONFIDENCE = smoothed
258
  return smoothed
259
 
260
+ def detect_market_regime(features: Dict[str, Any]) -> str:
261
  adx = features.get("adx", 20.0)
262
  volatility = features.get("volatility_1h", 0.0)
263
  hurst = features.get("hurst_exponent", 0.5)
264
+ if adx > 30 and hurst > 0.55:
265
+ return "TREND"
266
+ if volatility > 0.04:
267
+ return "VOLATILE"
268
  return "RANGE"
269
 
270
+ def stress_test(features: Dict[str, Any]) -> Optional[str]:
271
+ if features.get("atr_pct", 3.0) > 12.0:
272
+ return "WAIT"
273
+ if features.get("volatility_1h", 0.0) > 0.08:
274
+ return "WAIT"
275
  return None
276
 
277
  # ================= ЗАГРУЗКА ДАННЫХ =================
278
+ def fetch_twelvedata_sol(tf: str = "1h") -> Tuple[Optional[pd.DataFrame], Optional[str]]:
279
  cache_key = f"td_sol_{tf}"
280
  if cache_key in DATA_CACHE:
281
  age = time.time() - DATA_CACHE[cache_key].get("timestamp",0)
282
+ if age < CACHE_TTL:
283
+ return DATA_CACHE[cache_key]["df"], DATA_CACHE[cache_key]["source"]
284
  for key_idx, key in enumerate(TWELVE_KEYS):
285
  try:
286
  url = f"https://api.twelvedata.com/time_series?symbol=SOL/USD&interval={tf}&outputsize=200&apikey={key}"
 
289
  data = r.json()
290
  if "values" in data:
291
  df = pd.DataFrame(data["values"]).iloc[::-1].reset_index(drop=True)
292
+ for col in ["close","high","low","open"]:
293
+ df[col] = pd.to_numeric(df[col], errors="coerce")
294
  df["volume"] = pd.to_numeric(df.get("volume",0), errors="coerce").fillna(0)
295
  df = df.dropna(subset=["close","high","low","open"])
296
  if len(df) >= 30:
297
+ DATA_CACHE[cache_key] = {
298
+ "df":df,
299
+ "source":f"TwelveData-Key{key_idx+1}",
300
+ "timestamp":time.time()
301
+ }
302
  return df, f"TwelveData-Key{key_idx+1}"
303
+ elif r.status_code == 429:
304
+ continue
305
+ except:
306
+ continue
307
  if HAS_YFINANCE:
308
  try:
309
  yf_interval = YAHOO_INTERVAL_MAP.get(tf, "60m")
310
  period_map = {"15min":"7d", "1h":"60d", "4h":"60d"}
311
  yf_data = yf.download("SOL-USD", period=period_map.get(tf,"60d"), interval=yf_interval, progress=False)
312
  if not yf_data.empty:
313
+ df = pd.DataFrame({
314
+ 'close': yf_data['Close'].values.flatten(),
315
+ 'high': yf_data['High'].values.flatten(),
316
+ 'low': yf_data['Low'].values.flatten(),
317
+ 'open': yf_data['Open'].values.flatten(),
318
+ 'volume': yf_data['Volume'].values.flatten()
319
+ }).dropna()
320
  if len(df) >= 30:
321
  DATA_CACHE[cache_key] = {"df":df, "source":"YahooFinance", "timestamp":time.time()}
322
  return df, "YahooFinance"
323
+ except:
324
+ pass
325
  return None, None
326
 
327
+ def fetch_solana_onchain() -> Dict[str, Any]:
328
+ """TVL, DEX volume, active addresses из DefiLlama."""
329
  cache_key = "solana_onchain"
330
  if cache_key in DATA_CACHE and time.time() - DATA_CACHE[cache_key].get("timestamp",0) < 300:
331
  return DATA_CACHE[cache_key]["data"]
332
  result = {}
333
+ # TVL
334
  try:
335
+ r = session.get("https://api.llama.fi/v2/tvl/solana", timeout=10)
336
  if r.status_code == 200:
337
  data = r.json()
338
  result['tvl'] = data.get('tvl',0)
339
  result['tvl_change_24h'] = data.get('change_1d',0)
340
  result['tvl_trend'] = 'UP' if data.get('change_1d',0) > 0 else 'DOWN'
341
+ except:
342
+ pass
343
+ # DEX volume
344
  try:
345
+ r = session.get("https://api.llama.fi/overview/dexs/solana?excludeTotalDataChart=true&excludeTotalDataChartBreakdown=true", timeout=10)
346
  if r.status_code == 200:
347
  data = r.json()
348
  result['dex_volume_24h'] = data.get('total24h',0)
349
  result['dex_change_24h'] = data.get('change_1d',0)
350
+ except:
351
+ pass
352
+ # Active users (заглушка, т.к. DefiLlama не даёт active users напрямую, используем общий TVL как прокси)
353
+ result['active_users'] = result.get('tvl', 0) / 100 # прокси
 
 
 
354
  DATA_CACHE[cache_key] = {"data": result, "timestamp": time.time()}
355
  return result
356
 
357
+ def fetch_solana_dev_activity() -> Dict[str, Any]:
358
+ """Активность разработчиков из CoinGecko."""
359
  try:
360
+ r = session.get("https://api.coingecko.com/api/v3/coins/solana?developer_data=true", timeout=10)
361
  if r.status_code == 200:
362
+ dev = r.json().get('developer_data', {})
363
+ return {
364
+ 'developer_score': dev.get('developer_score', 0),
365
+ 'developer_activity': 'HIGH' if dev.get('developer_score',0) > 80 else 'MODERATE' if dev.get('developer_score',0) > 50 else 'LOW'
366
+ }
367
+ except:
368
+ pass
369
+ return {'developer_score': 50, 'developer_activity': 'MODERATE'}
 
 
 
 
 
 
 
 
 
 
 
370
 
371
+ def fetch_binance_sol() -> Dict[str, Any]:
372
+ """Funding rate и Open Interest из Binance Futures."""
373
  result = {}
374
  try:
375
+ r = session.get("https://fapi.binance.com/fapi/v1/premiumIndex", timeout=10)
376
  if r.status_code == 200:
377
  for item in r.json():
378
  if item.get('symbol') == 'SOLUSDT':
379
  fr = float(item.get('lastFundingRate',0))
380
  result['funding_rate'] = fr
381
  result['funding_signal'] = 'BEARISH' if fr > 0.001 else 'BULLISH' if fr < -0.001 else 'NEUTRAL'
382
+ break
383
+ except:
384
+ pass
385
  try:
386
+ r = session.get("https://fapi.binance.com/fapi/v1/openInterest?symbol=SOLUSDT", timeout=10)
387
+ if r.status_code == 200:
388
+ result['open_interest'] = float(r.json().get('openInterest',0))
389
+ except:
390
+ pass
391
  return result
392
 
393
+ def fetch_coingecko_sol() -> Dict[str, Any]:
394
+ try:
395
+ r = session.get("https://api.coingecko.com/api/v3/coins/solana?localization=false&tickers=false&community_data=false&developer_data=false", timeout=10)
396
+ if r.status_code == 200:
397
+ md = r.json().get('market_data', {})
398
+ return {
399
+ 'market_cap': md.get('market_cap',{}).get('usd',0),
400
+ 'total_volume': md.get('total_volume',{}).get('usd',0),
401
+ 'price_change_24h': md.get('price_change_percentage_24h',0)
402
+ }
403
+ except:
404
+ pass
405
+ return {'market_cap':0, 'total_volume':0, 'price_change_24h':0}
406
+
407
+ def fetch_space_signal(name: str, url: str, endpoint: str = "/consilium") -> Dict[str, Any]:
408
+ if breaker_open(name):
409
+ return {"active": False, "reason": "circuit_breaker"}
410
+ try:
411
+ r = session.get(f"{url}{endpoint}", timeout=8)
412
+ if r.status_code == 200:
413
+ return {"active": True, "data": r.json()}
414
+ else:
415
+ breaker_fail(name)
416
+ return {"active": False, "reason": f"status_{r.status_code}"}
417
+ except Exception as e:
418
+ breaker_fail(name)
419
+ return {"active": False, "reason": str(e)[:50]}
420
+
421
+ def send_to_arbiter(signal_data: Dict[str, Any]) -> None:
422
+ try:
423
+ requests.post(
424
+ f"{SPACE_URLS['space_18_arbiter']}/log_signal",
425
+ json={"space": "space_19_sol", "symbol": SYMBOL, "signal": signal_data.get("signal", {})},
426
+ timeout=5
427
+ )
428
+ except:
429
+ pass
430
+
431
+ # ================= ПОСТРОЕНИЕ ПРИЗНАКОВ =================
432
+ def build_features_from_mt5(mt5_features: Dict[str, Any]) -> Dict[str, Any]:
433
+ features: Dict[str, Any] = {}
434
  for k, v in mt5_features.items():
435
+ if isinstance(v, (int, float, np.floating, np.integer)):
436
+ features[k] = float(v)
437
+ elif isinstance(v, np.bool_):
438
+ features[k] = bool(v)
439
+ else:
440
+ features[k] = v
441
+ while len(features) < 200:
442
+ features[f"mt5_pad_{len(features)}"] = 0.0
443
  return features
444
 
445
+ def build_sol_features(df: pd.DataFrame, onchain_data: Optional[Dict[str, Any]] = None,
446
+ dev_data: Optional[Dict[str, Any]] = None,
447
+ derivatives: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
448
+ if df is None or len(df) < 20:
449
+ return {}
450
+
451
  try:
452
+ close = df["close"].astype(float)
453
+ high = df["high"].astype(float)
454
+ low = df["low"].astype(float)
455
+ open_p = df["open"].astype(float) if "open" in df.columns else close
456
  volume = df["volume"].astype(float) if "volume" in df.columns else pd.Series([0.0]*len(df))
457
+ except:
458
+ return {}
459
+
460
+ features: Dict[str, Any] = {}
461
  features["price"] = safe_float(close.iloc[-1])
462
  features["return_1h"] = safe_float(close.pct_change(1).iloc[-1])
463
+ features["return_24h"] = safe_float(close.pct_change(24).iloc[-1]) if len(close) > 24 else 0.0
464
+
465
  kf = KalmanFilter()
466
  kalman_close = [kf.update(x) for x in close.values]
467
  features["kalman_price"] = kalman_close[-1]
468
  features["kalman_diff"] = close.iloc[-1] - kalman_close[-1]
469
+
470
  features["hurst_exponent"] = hurst_exponent(close)
471
+
472
  ret = close.pct_change()
473
+ features["volatility_1h"] = safe_float(ret.rolling(24, min_periods=24).std().iloc[-1]) if len(close) >= 24 else 0.0
474
+ features["high_low_ratio"] = safe_float(((high.iloc[-1] - low.iloc[-1]) / (close.iloc[-1] + 1e-10)) * 100)
475
+
476
+ for span in [9, 21, 50]:
477
  if len(close) >= span:
478
  _, ema_val = safe_ema(close, span)
479
  if ema_val != 0:
480
  features[f"ema_{span}"] = ema_val
481
+ features[f"price_vs_ema_{span}"] = safe_float(((close.iloc[-1] - ema_val) / ema_val) * 100)
482
+
483
  if len(close) >= 26:
484
  try:
485
  ema12 = close.ewm(span=12, adjust=False).mean()
 
489
  features["macd"] = safe_float(macd.iloc[-1])
490
  features["macd_signal"] = safe_float(signal.iloc[-1])
491
  features["macd_hist"] = features["macd"] - features["macd_signal"]
492
+ except:
493
+ pass
494
+
495
+ features["rsi_14"] = safe_rsi(close, 14) if len(close) >= 14 else 50.0
496
+
497
  if len(close) >= 14:
498
  try:
499
  prev_close = close.shift(1)
500
+ tr = pd.DataFrame({
501
+ "tr1": high - low,
502
+ "tr2": (high - prev_close).abs(),
503
+ "tr3": (low - prev_close).abs()
504
+ }).max(axis=1)
505
  features["atr_14"] = safe_float(tr.rolling(14, min_periods=14).mean().iloc[-1])
506
+ features["atr_pct"] = (features["atr_14"] / (close.iloc[-1] + 1e-10)) * 100
507
+ except:
508
+ features["atr_14"] = close.iloc[-1] * 0.02
509
+
510
+ # Ончейн и фундаментальные данные
511
  if onchain_data:
512
+ features["tvl"] = onchain_data.get("tvl", 0)
513
+ features["tvl_trend"] = 1 if onchain_data.get("tvl_trend") == "UP" else -1
514
+ features["dex_volume_24h"] = onchain_data.get("dex_volume_24h", 0)
515
+ features["active_users"] = onchain_data.get("active_users", 0)
516
+ if dev_data:
517
+ features["developer_score"] = dev_data.get("developer_score", 50)
518
+ features["developer_activity"] = dev_data.get("developer_activity", "MODERATE")
519
+ if derivatives:
520
+ features["funding_rate"] = derivatives.get("funding_rate", 0)
521
+ features["open_interest"] = derivatives.get("open_interest", 0)
522
+ features["funding_bullish"] = 1 if derivatives.get("funding_signal") == "BULLISH" else 0
523
+ features["funding_bearish"] = 1 if derivatives.get("funding_signal") == "BEARISH" else 0
524
+
525
+ for kw in ["solana", "memecoin", "pump_fun", "firedancer"]:
526
+ features[f"trends_{kw}"] = fetch_google_trends_index(kw)
527
+
528
  now = datetime.utcnow()
529
+ features["is_weekend"] = 1 if now.weekday() >= 5 else 0
530
  features["hour"] = now.hour
531
+
532
+ while len(features) < 200:
533
+ features[f"pad_{len(features)}"] = 0.0
534
+
535
  return features
536
 
537
+ def get_multi_tf_features(onchain_data: Dict[str, Any], dev_data: Dict[str, Any],
538
+ derivatives: Dict[str, Any]) -> Tuple[Dict[str, Dict[str, Any]], List[str]]:
539
+ all_features: Dict[str, Dict[str, Any]] = {}
540
+ sources: List[str] = []
541
  for tf in TIMEFRAMES:
542
  df, source = fetch_twelvedata_sol(tf)
543
  if df is not None and len(df) >= 30:
544
+ feats = build_sol_features(df, onchain_data, dev_data, derivatives)
545
+ if feats:
546
+ all_features[tf] = feats
547
+ sources.append(source or "Unknown")
548
  return all_features, sources
549
 
550
+ # ================= ДИНАМИЧЕСКИЕ ВЕСА =================
551
+ def update_component_perf(component: str, success: bool):
552
+ c = COMPONENT_PERF[component]
553
+ c["total"] += 1
554
+ if success:
555
+ c["correct"] += 1
556
+ if success:
557
+ c["sharpe"] = min(3.0, c["sharpe"] + 0.1)
558
+ else:
559
+ c["sharpe"] = max(0.1, c["sharpe"] - 0.1)
560
+
561
+ def get_dynamic_component_weights(regime: str) -> Dict[str, float]:
562
+ base = REGIME_WEIGHTS.get(regime, REGIME_WEIGHTS["RANGE"])
563
+ model_acc = COMPONENT_PERF["model"]["correct"] / max(COMPONENT_PERF["model"]["total"], 1)
564
+ model_sharpe = COMPONENT_PERF["model"]["sharpe"]
565
+ model_w = base["model"] * model_acc * model_sharpe
566
+
567
+ tf_acc = COMPONENT_PERF["tf"]["correct"] / max(COMPONENT_PERF["tf"]["total"], 1)
568
+ tf_sharpe = COMPONENT_PERF["tf"]["sharpe"]
569
+ tf_w = base["tf"] * tf_acc * tf_sharpe
570
+
571
+ remaining = 1.0 - (model_w + tf_w)
572
+ onchain_w = remaining * 0.6
573
+ deriv_w = remaining * 0.4
574
+
575
+ weights = {"model": model_w, "tf": tf_w, "onchain": onchain_w, "derivatives": deriv_w}
576
+ norm = sum(weights.values())
577
+ if norm > 0:
578
+ weights = {k: v/norm for k, v in weights.items()}
579
+ return weights
580
+
581
+ # ================= ГЛАВНЫЙ СИГНАЛ (АНСАМБЛЬ С DYNAMIC WEIGHTS) =================
582
+ def get_sol_signal() -> Optional[Dict[str, Any]]:
583
  global LAST_CONFIDENCE
584
  start = time.time()
585
+
586
+ # Дополнительные данные (всегда)
587
+ onchain_data = fetch_solana_onchain()
588
+ dev_data = fetch_solana_dev_activity()
589
+ coingecko = fetch_coingecko_sol()
590
+ binance_data = fetch_binance_sol()
591
+
592
+ # MT5 или API
593
+ mt5_features: Optional[Dict[str, Any]] = None
594
+ mt5_price: Optional[float] = None
595
+ data_source: str = "UNKNOWN"
596
+
597
  fs = FEATURES_STORE.get(SYMBOL, {})
598
  age = time.time() - fs.get("timestamp", 0)
599
  if age < MT5_MAX_AGE_SEC:
600
+ mt5_features = fs.get("features", {})
601
+ mt5_price = fs.get("price")
602
+ data_source = "MT5"
603
+ print(f"📡 Используем MT5 данные (возраст {age:.0f}с)")
604
+
605
+ mtf_features: Dict[str, Dict[str, Any]] = {}
606
+ sources: List[str] = []
607
+
608
  if mt5_features and len(mt5_features) >= 50:
609
  model_features = build_features_from_mt5(mt5_features)
610
+ # Добавляем доп. данные
611
+ model_features["tvl"] = onchain_data.get("tvl", 0)
612
+ model_features["tvl_trend"] = 1 if onchain_data.get("tvl_trend") == "UP" else -1
613
+ model_features["active_users"] = onchain_data.get("active_users", 0)
614
+ model_features["developer_score"] = dev_data.get("developer_score", 50)
615
+ model_features["funding_rate"] = binance_data.get("funding_rate", 0)
616
+ model_features["open_interest"] = binance_data.get("open_interest", 0)
617
+ model_features["funding_bullish"] = 1 if binance_data.get("funding_signal") == "BULLISH" else 0
618
+ model_features["funding_bearish"] = 1 if binance_data.get("funding_signal") == "BEARISH" else 0
619
+ price = mt5_price or model_features.get("H1_price", model_features.get("price", 0))
620
  sources = ["MT5"]
621
  else:
622
+ print(" ⚠️ MT5 данные недоступны, перехожу на API...")
623
+ mtf_features, sources = get_multi_tf_features(onchain_data, dev_data, binance_data)
624
+ if not mtf_features:
625
+ print("❌ Нет данных")
626
+ return None
627
  h1_features = mtf_features.get("1h", list(mtf_features.values())[0])
628
  model_features = h1_features
629
+ price = h1_features.get("price", 0)
630
  data_source = "+".join(sources) if sources else "API"
631
+
632
+ if price == 0:
633
+ return None
634
+
635
+ # Стресс-тест
636
  stress = stress_test(model_features)
637
  if stress == "WAIT":
638
+ print("🛑 СТРЕСС-ТЕСТ: рынок слишком опасен")
639
+ return {
640
+ "space": "space_19_sol_master",
641
+ "symbol": SYMBOL,
642
+ "signal": {"direction": "WAIT", "confidence": 0.0},
643
+ "reason": "stress_test_black_swan"
644
+ }
645
+
646
  regime = detect_market_regime(model_features)
647
+ print(f"📊 Режим: {regime} | Цена: ${price:.2f} | TVL: ${onchain_data.get('tvl',0)/1e6:.1f}M | Данные: {data_source}")
648
+
649
+ # === ПРЕДСКАЗАНИЕ МОДЕЛИ (АНСАМБЛЬ daily + 4h) ===
650
  xgb_prob = 0.5
651
+ models_used = 0
652
  if model_features:
653
  try:
654
  fv = list(model_features.values())[:200]
655
  while len(fv) < 200:
656
  fv.append(0.0)
657
  X = np.nan_to_num(np.array(fv, dtype=np.float64).reshape(1, -1))
658
+
659
  probs = []
660
  if MODELS.get("xgb_daily"):
661
  proba = MODELS["xgb_daily"].predict_proba(X)[0]
662
  probs.append(float(proba[1] if len(proba) > 1 else proba[0]))
663
+ models_used += 1
664
  if MODELS.get("xgb_4h"):
665
  proba = MODELS["xgb_4h"].predict_proba(X)[0]
666
  probs.append(float(proba[1] if len(proba) > 1 else proba[0]))
667
+ models_used += 1
668
+ if MODELS.get("lgb"):
669
+ proba = MODELS["lgb"].predict_proba(X)[0]
670
+ lgb_prob = float(proba[1] if len(proba) > 1 else proba[0])
671
+ if probs:
672
+ xgb_prob = sum(probs)/len(probs) * 0.6 + lgb_prob * 0.4
673
+ else:
674
+ xgb_prob = lgb_prob
675
+ models_used += 1
676
+ elif probs:
677
  xgb_prob = sum(probs) / len(probs)
678
+ xgb_prob = max(0.0, min(1.0, xgb_prob))
679
  except Exception as e:
680
  print(f" ⚠️ Ошибка предсказания: {e}")
681
 
682
+ # Мульти-ТФ подтверждение
 
 
 
 
 
 
683
  if data_source == "MT5":
684
+ confirmations, total_tf = 0, 0
685
+ for tf_key in ["M15", "H1", "H4"]:
686
  ema_key = f"{tf_key}_price_vs_ema_21"
687
  if ema_key in model_features:
688
  total_tf += 1
689
+ if model_features.get(ema_key, 0) > 0:
690
+ confirmations += 1
691
+ else:
692
+ confirmations -= 1
693
+ tf_score = confirmations / max(total_tf, 1)
694
+ tf_norm = (tf_score + 1) / 2
695
  else:
696
+ confirmations, total_tf = 0, 0
697
+ for tf_key in ["15min", "1h", "4h"]:
698
  tf_feats = mtf_features.get(tf_key, {})
699
+ if not tf_feats:
700
+ continue
701
  total_tf += 1
702
+ ema_score = tf_feats.get("price_vs_ema_21", 0)
703
+ rsi_val = tf_feats.get("rsi_14", 50)
704
+ macd_hist = tf_feats.get("macd_hist", 0)
705
+ if ema_score > 0 and rsi_val > 50 and macd_hist > 0:
706
+ confirmations += 1
707
+ elif ema_score < 0 and rsi_val < 50 and macd_hist < 0:
708
+ confirmations -= 1
709
+ tf_score = confirmations / max(total_tf, 1)
710
+ tf_norm = (tf_score + 1) / 2
711
+
712
+ # Meta SOL Score: TVL, DEX Volume, Active Addresses, Developer Activity
713
+ tvl_norm = 0.5
714
+ if onchain_data.get("tvl", 0) > 0:
715
+ tvl_norm = min(1.0, max(0.0, 0.5 + onchain_data.get("tvl_change_24h", 0) / 20))
716
+ dex_norm = 0.5
717
+ if onchain_data.get("dex_volume_24h", 0) > 0:
718
+ dex_norm = min(1.0, max(0.0, 0.5 + onchain_data.get("dex_change_24h", 0) / 20))
719
+ dev_norm = 0.5
720
+ if dev_data.get("developer_score", 50) > 50:
721
+ dev_norm = 0.7
722
+ active_norm = 0.5
723
+ # Meta Score = среднее
724
+ meta_sol_score = (tvl_norm + dex_norm + dev_norm + active_norm) / 4
725
+
726
+ # Компонент деривативов
727
+ deriv_norm = 0.5
728
+ if binance_data.get("funding_signal") == "BULLISH":
729
+ deriv_norm = 0.7
730
+ elif binance_data.get("funding_signal") == "BEARISH":
731
+ deriv_norm = 0.3
732
+
733
+ # Динамические веса
734
+ comp_weights = get_dynamic_component_weights(regime)
735
+ print(f" ⚖️ Веса: {comp_weights}")
736
+
737
+ final_score = (
738
+ xgb_prob * comp_weights["model"] +
739
+ tf_norm * comp_weights["tf"] +
740
+ meta_sol_score * comp_weights["onchain"] +
741
+ deriv_norm * comp_weights["derivatives"]
742
+ )
743
  confidence = smooth_confidence(final_score)
744
 
745
+ if confidence > SOL_THRESHOLD + 0.08:
746
+ direction = "LONG"
747
+ elif confidence < SOL_THRESHOLD - 0.08:
748
+ direction = "SHORT"
749
+ else:
750
+ direction = "WAIT"
751
 
752
+ # SL/TP
753
+ atr = model_features.get("atr_14", price * 0.02)
754
  sl_mult = TRADING_RULES.get("sl_atr_multiplier", 2.0)
755
  tp_mult = TRADING_RULES.get("tp_atr_multiplier", 4.0)
756
+ sl_dist = atr * sl_mult
757
+ tp_dist = atr * tp_mult
758
+ if direction == "LONG":
759
+ sl = round(price - sl_dist, 2)
760
+ tp = round(price + tp_dist, 2)
761
+ elif direction == "SHORT":
762
+ sl = round(price + sl_dist, 2)
763
+ tp = round(price - tp_dist, 2)
764
+ else:
765
+ sl = tp = 0
766
 
767
+ PREDICTION_HISTORY.append({
768
+ "timestamp": datetime.utcnow().isoformat(),
769
+ "direction": direction,
770
+ "confidence": confidence,
771
+ "price": price
772
+ })
773
 
774
+ if db:
775
+ try:
776
+ db.collection("space19_sol_signals").add({
777
+ "direction": direction,
778
+ "confidence": confidence,
779
+ "price": price,
780
+ "regime": regime,
781
+ "data_source": data_source,
782
+ "timestamp": firestore.SERVER_TIMESTAMP
783
+ })
784
+ except:
785
+ pass
786
+
787
+ latency = int((time.time() - start) * 1000)
788
  result = {
789
+ "space": "space_19_sol_master",
790
+ "timestamp": int(time.time()),
791
+ "symbol": SYMBOL,
792
+ "signal": {
793
+ "direction": direction,
794
+ "confidence": round(confidence, 4),
795
+ "strength": round(confidence * (1 + abs(tf_score)), 4)
796
+ },
797
  "analysis": {
798
+ "xgb_probability": round(xgb_prob, 4),
799
+ "models_used": models_used,
800
+ "multi_tf_score": round(tf_score, 4),
801
+ "meta_sol_score": round(meta_sol_score, 4),
802
  "market_regime": regime,
803
  "data_source": data_source
804
  },
805
+ "onchain": {
806
+ "tvl": onchain_data.get("tvl", 0),
807
+ "dex_volume_24h": onchain_data.get("dex_volume_24h", 0),
808
+ "active_users": onchain_data.get("active_users", 0),
809
+ "developer_score": dev_data.get("developer_score", 50),
810
+ "funding_rate": binance_data.get("funding_rate", 0)
811
+ },
812
+ "risk": {"sl": sl, "tp": tp, "rr": round(tp_dist/(sl_dist+1e-10), 2) if direction != "WAIT" else 0},
813
+ "meta": {
814
+ "latency_ms": latency,
815
+ "model_version": "v8.0_solana_dominance",
816
+ "features_used": len(model_features) if model_features else 0,
817
+ "dynamic_weights": comp_weights
818
+ }
819
  }
820
+
821
+ send_to_arbiter(result)
822
+ print(f"🥉 SOL/USD: {direction} | conf={confidence:.3f} | ensemble={xgb_prob:.3f} | models={models_used} | regime={regime} | latency={latency}ms")
 
823
  return result
824
 
825
+ # ================= KEEP-ALIVE =================
826
  def keep_alive():
827
  while True:
828
  time.sleep(840)
829
+ try:
830
+ requests.get("http://localhost:7860/health", timeout=5)
831
+ except:
832
+ pass
833
  threading.Thread(target=keep_alive, daemon=True).start()
834
 
835
+ # ================= FASTAPI =================
836
+ app = FastAPI(title="TOMIRIS SOL/USD MASTER v8.0 Solana Dominance")
837
 
838
  @app.get("/health")
839
+ async def health():
840
+ models_loaded = sum(1 for m in ["xgb_daily","xgb_4h","lgb"] if MODELS.get(m) is not None)
841
  return {
842
+ "space": "Space 19 v8.0 Solana Dominance",
843
  "status": "operational",
844
  "symbol": SYMBOL,
845
  "models_loaded": models_loaded,
846
+ "dynamic_weights": True,
847
+ "features": ["TVL", "DEX Volume", "Active Addresses", "Developer Activity", "Funding Rate"]
848
  }
849
 
850
  @app.get("/consilium")
851
+ async def consilium():
852
  try:
853
  signal = get_sol_signal()
854
+ if signal:
855
+ return signal
856
+ return {"space":"space_19_sol_master","symbol":SYMBOL,"signal":{"direction":"WAIT","confidence":0.0},"error":"no_data"}
857
  except Exception as e:
858
  return {"space":"space_19_sol_master","symbol":SYMBOL,"signal":{"direction":"WAIT","confidence":0.0},"error": str(e)[:100]}
859
 
860
  @app.get("/signal")
861
+ async def signal():
862
+ return await consilium()
863
 
864
  @app.get("/onchain")
865
+ async def onchain():
866
+ return {
867
+ "solana": fetch_solana_onchain(),
868
+ "dev_activity": fetch_solana_dev_activity(),
869
+ "coingecko": fetch_coingecko_sol(),
870
+ "binance": fetch_binance_sol()
871
+ }
872
 
873
  @app.get("/price")
874
+ async def current_price():
875
  hub = get_mt5_price_from_hub()
876
  return {"symbol": SYMBOL, "price": hub["price"] if hub["fresh"] else HUB_CACHE.get("price",0.0), "source": hub["source"], "fresh": hub["fresh"]}
877
 
878
  @app.post("/features")
879
+ async def receive_features(data: Dict[str, Any]):
880
+ symbol = data.get("symbol", SYMBOL)
881
+ FEATURES_STORE[symbol] = {
882
+ "features": data.get("features", {}),
883
+ "price": data.get("price", 0.0),
884
+ "timestamp": time.time()
885
+ }
886
  print(f"📥 MT5 {symbol}: {len(data.get('features',{}))} признаков")
887
  return {"status": "ok"}
888
 
889
+ @app.get("/metrics")
890
+ async def metrics():
891
+ return {"symbol": SYMBOL, "predictions_stored": len(PREDICTION_HISTORY), "last_confidence": LAST_CONFIDENCE, "component_performance": COMPONENT_PERF}
892
+
893
+ @app.get("/explain")
894
+ async def explain():
895
+ if PREDICTION_HISTORY:
896
+ last = PREDICTION_HISTORY[-1]
897
+ return {
898
+ "last_direction": last["direction"],
899
+ "confidence": last["confidence"],
900
+ "note": "Explainability via dynamic weights"
901
+ }
902
+ return {"error": "No prediction yet"}
903
+
904
+ print("🚀 SPACE 19 v8.0 — SOL/USD MASTER (Solana Dominance) ЗАПУЩЕН!")
905
+ print("🥉 Ансамбль daily+4h + Meta SOL Score + Dynamic Weights")
906
  print("✅ Готов к бою!")