tomirisai80 commited on
Commit
e514d1f
·
verified ·
1 Parent(s): 2210be8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +234 -207
app.py CHANGED
@@ -1,11 +1,11 @@
1
  # ============================================
2
- # 👑 TOMIRIS SPACE 19 v11.0 «СТАЛЬ» — SOL/USD MASTER
3
  # ============================================
4
- import os, time, threading, warnings, json, asyncio, sqlite3
5
  from typing import Dict, Any, Optional, List, Tuple
6
  import numpy as np, pandas as pd
7
- import requests
8
- from datetime import datetime, timedelta
9
  from collections import deque
10
  from fastapi import FastAPI, Query
11
  import logging
@@ -31,20 +31,43 @@ TIMEFRAMES = ["15min", "1h", "4h"]
31
  HUB_URL = os.getenv("HUB_URL", "https://pro-3-tomiris-hub.hf.space")
32
  HUB_SECRET = os.getenv("HUB_SECRET", "TomyrisUltraSecret2026!")
33
 
34
- STARTUP_SLEEP = int(os.getenv("STARTUP_SLEEP", "600"))
35
- AUTO_REPORT_INTERVAL = 600
36
  SOL_THRESHOLD = 0.52
37
 
38
- CACHE_TTL = 600
39
  DATA_CACHE: Dict[str, Dict[str, Any]] = {}
40
  LAST_CONFIDENCE = 0.5
41
 
42
- logger.info(f"🔗 Хаб: {HUB_URL}")
 
 
 
43
 
44
  def hub_headers():
45
  return {"X-Hub-Secret": HUB_SECRET, "Content-Type": "application/json"}
46
 
47
- # ================= SQLite ДЛЯ СТАТИСТИКИ =================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  DB_FILE = "sol_master.db"
49
 
50
  def init_db():
@@ -62,9 +85,10 @@ def init_db():
62
  signal TEXT NOT NULL,
63
  confidence REAL,
64
  regime TEXT,
65
- models_used INTEGER
 
 
66
  )''')
67
- # Инициализируем дефолтные значения если таблица пуста
68
  for comp in ["model", "tf", "onchain", "derivatives"]:
69
  c.execute("INSERT OR IGNORE INTO component_perf (component, correct, total, sharpe) VALUES (?, 0, 1, 1.0)", (comp,))
70
  conn.commit()
@@ -83,8 +107,6 @@ def load_component_perf():
83
  perf[row[0]] = {"correct": row[1], "total": row[2], "sharpe": row[3]}
84
  conn.close()
85
  except: pass
86
-
87
- # Гарантируем что все компоненты есть
88
  for comp in ["model", "tf", "onchain", "derivatives"]:
89
  if comp not in perf:
90
  perf[comp] = {"correct": 0, "total": 1, "sharpe": 1.0}
@@ -104,31 +126,46 @@ def save_component_perf(perf: Dict):
104
  COMPONENT_PERF = load_component_perf()
105
 
106
  REGIME_WEIGHTS = {
107
- "TREND": {"model": 0.70, "tf": 0.30},
108
- "VOLATILE": {"model": 0.50, "tf": 0.50},
109
- "RANGE": {"model": 0.60, "tf": 0.40}
110
  }
111
 
112
  # ================= ЗАГРУЗКА МОДЕЛЕЙ =================
113
- logger.info(f"🔥 Загрузка моделей для {SYMBOL}...")
114
- MODELS: Dict[str, Optional[Any]] = {"xgb_daily": None, "xgb_4h": None, "lgb": None}
115
-
116
- if HAS_JOBLIB:
117
- for fname, key in [("xgboost_sol_daily.joblib", "xgb_daily"),
118
- ("xgboost_sol_4h.joblib", "xgb_4h"),
119
- ("lgb_sol.joblib", "lgb")]:
120
- if os.path.exists(fname):
121
- try:
122
- MODELS[key] = joblib.load(fname)
123
- logger.info(f" {fname} загружен")
124
- except Exception as e:
125
- logger.warning(f"⚠️ {fname}: {e}")
126
-
127
- # ================= HTTP СЕССИЯ =================
128
- session = requests.Session()
129
- session.headers.update({"User-Agent": "Tomiris-Space19-v11.0"})
130
-
131
- # 🔥 ФИКСИРОВАННЫЙ ПОРЯДОК ПРИЗНАКОВ!
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  FEATURE_ORDER = [
133
  "price", "return_1h", "return_24h", "hurst_exponent",
134
  "volatility_1h", "high_low_ratio",
@@ -136,7 +173,7 @@ FEATURE_ORDER = [
136
  "ema_50", "price_vs_ema_50",
137
  "macd", "macd_signal", "macd_hist",
138
  "rsi_14", "adx", "atr_14", "atr_pct",
139
- "tvl", "tvl_trend", "dex_volume_24h",
140
  "funding_rate", "open_interest",
141
  "funding_bullish", "funding_bearish",
142
  "is_weekend", "hour"
@@ -166,40 +203,19 @@ def safe_ema(close, span):
166
  except: return float(close.iloc[-1])
167
 
168
  def calculate_adx(df: pd.DataFrame, period: int = 14) -> float:
169
- """🔥 НАСТОЯЩИЙ ADX!"""
170
- if df is None or len(df) < period * 2:
171
- return 20.0
172
-
173
- high = df["high"].astype(float).values
174
- low = df["low"].astype(float).values
175
- close = df["close"].astype(float).values
176
-
177
- dm_plus = np.zeros(len(high))
178
- dm_minus = np.zeros(len(high))
179
- tr = np.zeros(len(high))
180
-
181
  for i in range(1, len(high)):
182
- # True Range
183
  tr[i] = max(high[i] - low[i], abs(high[i] - close[i-1]), abs(low[i] - close[i-1]))
184
-
185
- # Directional Movement
186
- up_move = high[i] - high[i-1]
187
- down_move = low[i-1] - low[i]
188
-
189
- if up_move > down_move and up_move > 0:
190
- dm_plus[i] = up_move
191
- if down_move > up_move and down_move > 0:
192
- dm_minus[i] = down_move
193
-
194
- # Сглаживание за 14 периодов
195
  atr = np.mean(tr[-period:]) if np.mean(tr[-period:]) > 0 else 0.001
196
  di_plus = 100 * np.mean(dm_plus[-period:]) / atr
197
  di_minus = 100 * np.mean(dm_minus[-period:]) / atr
198
-
199
  dx_sum = di_plus + di_minus
200
- if dx_sum > 0:
201
- adx = abs(di_plus - di_minus) / dx_sum * 100
202
- return float(adx)
203
  return 20.0
204
 
205
  def hurst_exponent(series, lags=20):
@@ -224,19 +240,17 @@ def detect_market_regime(features):
224
  if volatility > 0.04: return "VOLATILE"
225
  return "RANGE"
226
 
227
- # ================= ЗАГРУЗКА ДАННЫХ (ТОЛЬКО ХАБ!) =================
228
- def fetch_ohlc_hub(symbol, tf, limit=200):
229
  cache_key = f"hub_{symbol}_{tf}"
230
  if cache_key in DATA_CACHE and time.time() - DATA_CACHE[cache_key].get("timestamp", 0) < CACHE_TTL:
231
  return DATA_CACHE[cache_key]["df"]
232
 
233
  try:
234
- # 🔥 Используем оба параметра для совместимости
235
- r = session.get(
236
  f"{HUB_URL}/candles",
237
  params={"symbol": symbol, "interval": tf, "timeframe": tf, "limit": limit},
238
- timeout=30,
239
- headers=hub_headers()
240
  )
241
  if r.status_code == 200:
242
  candles = r.json().get("candles", [])
@@ -254,17 +268,14 @@ def fetch_ohlc_hub(symbol, tf, limit=200):
254
  logger.warning(f"Hub {symbol} {tf}: {e}")
255
  return None
256
 
257
- def fetch_ohlc(symbol, tf="1h"):
258
- return fetch_ohlc_hub(symbol, tf)
259
-
260
- def fetch_solana_onchain():
261
  cache_key = "solana_onchain"
262
  if cache_key in DATA_CACHE and time.time() - DATA_CACHE[cache_key].get("timestamp", 0) < CACHE_TTL:
263
  return DATA_CACHE[cache_key]["data"]
264
 
265
  result = {"tvl": 0, "tvl_change_24h": 0, "dex_volume_24h": 0, "dex_change_24h": 0}
266
  try:
267
- r = session.get("https://api.llama.fi/tvl/solana", timeout=10)
268
  if r.status_code == 200:
269
  text = r.text.strip()
270
  if text:
@@ -274,51 +285,55 @@ def fetch_solana_onchain():
274
  result['tvl'] = float(data)
275
  elif isinstance(data, dict):
276
  result['tvl'] = data.get('tvl', 0)
277
- result['tvl_change_24h'] = data.get('change_1d', 0)
278
  except: pass
 
 
 
 
 
 
 
279
  except: pass
280
 
281
  result['tvl_trend'] = 'UP' if result.get('tvl_change_24h', 0) > 0 else 'DOWN'
282
  DATA_CACHE[cache_key] = {"data": result, "timestamp": time.time()}
283
  return result
284
 
285
- def fetch_binance_sol():
286
  result = {}
287
  try:
288
- r = session.get("https://fapi.binance.com/fapi/v1/premiumIndex", timeout=10)
289
  if r.status_code == 200:
290
- for item in r.json():
291
- if item.get('symbol') == 'SOLUSDT':
292
- result['funding_rate'] = float(item.get('lastFundingRate', 0))
293
- # 🔥 Более умная трактовка funding rate
294
- fr = result['funding_rate']
295
- if fr > 0.005: # Очень высокий → возможен разворот
296
- result['funding_signal'] = 'CAUTION_LONG'
297
- elif fr > 0.001: # Высокий → бычий рынок, но осторожно
298
- result['funding_signal'] = 'BULLISH'
299
- elif fr < -0.005:
300
- result['funding_signal'] = 'CAUTION_SHORT'
301
- elif fr < -0.001:
302
- result['funding_signal'] = 'BEARISH'
303
- else:
304
- result['funding_signal'] = 'NEUTRAL'
305
- break
306
  except: pass
 
307
  try:
308
- r = session.get("https://fapi.binance.com/fapi/v1/openInterest?symbol=SOLUSDT", timeout=10)
309
  if r.status_code == 200:
310
  result['open_interest'] = float(r.json().get('openInterest', 0))
311
  except: pass
312
  return result
313
 
314
- # ================= ПОСТРОЕНИЕ ПРИЗНАКОВ (ФИКСИРОВАННЫЙ ПОРЯДОК!) =================
315
  def build_sol_features(df, onchain_data=None, derivatives=None) -> Dict:
316
- if df is None or len(df) < 20:
317
- return {}
318
 
319
- close = df["close"].astype(float)
320
- high = df["high"].astype(float)
321
- low = df["low"].astype(float)
322
 
323
  features = {}
324
  features["price"] = safe_float(close.iloc[-1])
@@ -336,19 +351,14 @@ def build_sol_features(df, onchain_data=None, derivatives=None) -> Dict:
336
  features[f"price_vs_ema_{span}"] = safe_float(((close.iloc[-1] - ema_val) / ema_val) * 100) if ema_val != 0 else 0
337
 
338
  if len(close) >= 26:
339
- ema12 = close.ewm(span=12, adjust=False).mean()
340
- ema26 = close.ewm(span=26, adjust=False).mean()
341
- macd = ema12 - ema26
342
- signal = macd.ewm(span=9, adjust=False).mean()
343
- features["macd"] = safe_float(macd.iloc[-1])
344
- features["macd_signal"] = safe_float(signal.iloc[-1])
345
  features["macd_hist"] = features["macd"] - features["macd_signal"]
346
  else:
347
  features["macd"] = features["macd_signal"] = features["macd_hist"] = 0.0
348
 
349
  features["rsi_14"] = safe_rsi(close, 14) if len(close) >= 14 else 50.0
350
-
351
- # 🔥 НАСТОЯЩИЙ ADX!
352
  features["adx"] = calculate_adx(df, 14)
353
 
354
  if len(close) >= 14:
@@ -357,15 +367,15 @@ def build_sol_features(df, onchain_data=None, derivatives=None) -> Dict:
357
  features["atr_14"] = safe_float(tr.rolling(14, min_periods=14).mean().iloc[-1])
358
  features["atr_pct"] = (features["atr_14"] / (close.iloc[-1] + 1e-10)) * 100
359
  else:
360
- features["atr_14"] = close.iloc[-1] * 0.03
361
- features["atr_pct"] = 3.0
362
 
363
  if onchain_data:
364
  features["tvl"] = onchain_data.get("tvl", 0)
365
- features["tvl_trend"] = 1 if onchain_data.get("tvl_trend") == "UP" else -1
366
  features["dex_volume_24h"] = onchain_data.get("dex_volume_24h", 0)
 
367
  else:
368
- features["tvl"] = features["tvl_trend"] = features["dex_volume_24h"] = 0
369
 
370
  if derivatives:
371
  features["funding_rate"] = derivatives.get("funding_rate", 0)
@@ -377,11 +387,10 @@ def build_sol_features(df, onchain_data=None, derivatives=None) -> Dict:
377
  features["funding_rate"] = features["open_interest"] = 0
378
  features["funding_bullish"] = features["funding_bearish"] = 0
379
 
380
- now = datetime.utcnow()
381
  features["is_weekend"] = 1 if now.weekday() >= 5 else 0
382
  features["hour"] = now.hour
383
 
384
- # 🔥 ГАРАНТИРУЕМ ФИКСИРОВАННЫЙ ПОРЯДОК!
385
  ordered = {}
386
  for key in FEATURE_ORDER:
387
  ordered[key] = features.get(key, 0.0)
@@ -389,198 +398,216 @@ def build_sol_features(df, onchain_data=None, derivatives=None) -> Dict:
389
  return ordered
390
 
391
  # ================= ОТПРАВКА СИГНАЛА =================
392
- def send_signal_to_hub(signal: str, confidence: float, features: Dict = None):
393
- if features is None:
394
- features = {}
395
-
396
  payload = {
397
- "space_id": SPACE_ID,
398
- "space_name": SPACE_NAME,
399
- "symbol": SYMBOL,
400
- "signal": signal,
401
- "confidence": round(confidence, 4),
402
- "features": features,
403
- "metadata": {"version": "11.0"},
404
- "timestamp": datetime.now().isoformat()
405
  }
406
-
407
  for attempt in range(3):
408
  try:
409
- headers = hub_headers()
410
- r = session.post(f"{HUB_URL}/signals", json=payload, timeout=30, headers=headers)
411
  if r.status_code == 200:
412
  logger.info(f"📤 {SYMBOL}: {signal} conf={confidence:.3f}")
413
  return True
414
- elif r.status_code == 429:
415
- time.sleep(5 * (attempt + 1))
416
- else:
417
- time.sleep(3)
418
- except:
419
- time.sleep(3)
420
  return False
421
 
422
- # ================= СИГНАЛ =================
423
- def get_sol_signal():
424
  global LAST_CONFIDENCE
425
  start = time.time()
426
 
427
- onchain_data = fetch_solana_onchain()
428
- binance_data = fetch_binance_sol()
429
 
430
  all_features = {}
431
  for tf in TIMEFRAMES:
432
- df = fetch_ohlc(SYMBOL, tf)
433
  if df is not None and len(df) >= 30:
434
  feats = build_sol_features(df, onchain_data, binance_data)
435
  if feats:
436
  all_features[tf] = feats
437
 
438
  if not all_features:
439
- send_signal_to_hub("WAIT", 0.0, {"reason": "no_data"})
440
  return None
441
 
442
  h1_features = all_features.get("1h", list(all_features.values())[0])
443
  price = h1_features.get("price", 0)
444
- if price == 0:
445
- return None
446
 
447
  regime = detect_market_regime(h1_features)
 
448
 
449
- # 🔥 ML предсказание с фиксированным порядком признаков!
450
  xgb_prob = 0.5
451
  models_used = 0
 
 
452
  try:
453
  X = np.array([h1_features.get(f, 0.0) for f in FEATURE_ORDER], dtype=np.float64).reshape(1, -1)
454
  X = np.nan_to_num(X)
455
 
456
- probs = []
457
- for mk in ["xgb_daily", "xgb_4h"]:
458
- if MODELS.get(mk):
459
  try:
460
- proba = MODELS[mk].predict_proba(X)[0]
461
- probs.append(float(proba[1] if len(proba) > 1 else proba[0]))
 
462
  models_used += 1
463
  except: pass
464
 
465
- if MODELS.get("lgb"):
466
- try:
467
- proba = MODELS["lgb"].predict_proba(X)[0]
468
- lgb_prob = float(proba[1] if len(proba) > 1 else proba[0])
469
- xgb_prob = (sum(probs)/len(probs) * 0.6 + lgb_prob * 0.4) if probs else lgb_prob
470
- models_used += 1
471
- except: pass
472
- elif probs:
473
- xgb_prob = sum(probs) / len(probs)
474
  except: pass
475
 
476
- # Мульти-ТФ
477
  confirmations, total_tf = 0, 0
478
  for tf, feats in all_features.items():
479
  total_tf += 1
480
  ema_score = feats.get("price_vs_ema_21", 0)
481
  rsi_val = feats.get("rsi_14", 50)
482
  macd_hist = feats.get("macd_hist", 0)
483
- if ema_score > 0 and rsi_val > 50 and macd_hist > 0:
484
- confirmations += 1
485
- elif ema_score < 0 and rsi_val < 50 and macd_hist < 0:
486
- confirmations -= 1
487
  tf_norm = ((confirmations / max(total_tf, 1)) + 1) / 2
488
 
489
- # Meta SOL Score
490
- tvl_norm = min(1.0, max(0.0, 0.5 + onchain_data.get("tvl_change_24h", 0) / 20))
491
- meta_sol_score = tvl_norm # Упрощённо
 
 
492
 
493
- deriv_norm = 0.5
494
  fs = binance_data.get("funding_signal", "NEUTRAL")
495
- if fs == "BULLISH": deriv_norm = 0.7
496
- elif fs == "BEARISH": deriv_norm = 0.3
497
- elif fs == "CAUTION_LONG": deriv_norm = 0.4
498
- elif fs == "CAUTION_SHORT": deriv_norm = 0.6
 
499
 
500
- # 🔥 ФИКС: Проверка что total > 0 перед делением!
501
  base = REGIME_WEIGHTS.get(regime, REGIME_WEIGHTS["RANGE"])
502
  model_perf = COMPONENT_PERF.get("model", {"correct": 0, "total": 1})
503
  tf_perf = COMPONENT_PERF.get("tf", {"correct": 0, "total": 1})
504
 
505
- # Если нет статистики используем базовые веса
506
- if model_perf["total"] <= 1:
507
- model_w = base["model"]
508
- tf_w = base["tf"]
509
- else:
510
- model_acc = model_perf["correct"] / max(model_perf["total"], 1)
511
- tf_acc = tf_perf["correct"] / max(tf_perf["total"], 1)
512
- model_w = base["model"] * max(model_acc, 0.3) # Минимум 30% веса
513
- tf_w = base["tf"] * max(tf_acc, 0.3)
514
 
 
 
515
  remaining = 1.0 - (model_w + tf_w)
516
- onchain_w = remaining * 0.6
517
- deriv_w = remaining * 0.4
518
  total_w = model_w + tf_w + onchain_w + deriv_w
519
  if total_w > 0:
520
  model_w /= total_w; tf_w /= total_w; onchain_w /= total_w; deriv_w /= total_w
521
 
522
- final_score = xgb_prob * model_w + tf_norm * tf_w + meta_sol_score * onchain_w + deriv_norm * deriv_w
523
  confidence = smooth_confidence(final_score)
524
 
525
- if confidence > SOL_THRESHOLD + 0.08:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
526
  signal = "BUY"
527
- elif confidence < SOL_THRESHOLD - 0.08:
528
  signal = "SELL"
529
  else:
530
  signal = "WAIT"
531
 
532
- features_out = {"ml_prob": xgb_prob, "tf_norm": tf_norm, "regime": regime, "models_used": models_used}
533
- send_signal_to_hub(signal, confidence, features_out)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
534
 
535
  # Логируем в SQLite
536
  try:
537
  conn = sqlite3.connect(DB_FILE)
538
  c = conn.cursor()
539
- c.execute("INSERT INTO signals_log (timestamp, signal, confidence, regime, models_used) VALUES (?, ?, ?, ?, ?)",
540
- (datetime.now().isoformat(), signal, confidence, regime, models_used))
541
  conn.commit()
542
  conn.close()
543
  except: pass
544
 
545
- logger.info(f"🥉 SOL/USD: {signal} | conf={confidence:.3f} | models={models_used} | regime={regime} | adx={h1_features.get('adx', 0):.1f}")
546
- return {"signal": signal, "confidence": confidence}
 
 
547
 
548
- def auto_report():
 
549
  logger.info(f"⏳ Стартовый сон {STARTUP_SLEEP}с...")
550
- time.sleep(STARTUP_SLEEP)
 
551
  logger.info("✅ SOL Master — начинаю авто-отправку!")
552
  while True:
553
- time.sleep(AUTO_REPORT_INTERVAL)
554
  try:
555
- get_sol_signal()
556
  except Exception as e:
557
  logger.error(f"Ошибка: {e}")
558
 
559
- threading.Thread(target=auto_report, daemon=True).start()
 
560
 
561
- app = FastAPI(title="SOL Master v11.0 STEEL")
 
 
 
562
 
563
  @app.get("/health")
564
  async def health():
565
- return {"space_id": SPACE_ID, "status": "ok", "version": "11.0", "symbol": SYMBOL, "models": sum(1 for m in MODELS.values() if m is not None)}
566
 
567
  @app.head("/health")
568
- async def health_head():
569
- return {}
570
 
571
  @app.get("/consilium")
572
  async def consilium():
573
- result = get_sol_signal()
574
- if result:
575
- return {"signal": result["signal"], "confidence": result["confidence"]}
576
  return {"signal": "WAIT", "confidence": 0.0}
577
 
578
  @app.get("/")
579
  async def root():
580
- return {"name": "SOL Master v11.0 STEEL", "space_id": SPACE_ID, "hub": HUB_URL}
581
 
582
  if __name__ == "__main__":
583
  import uvicorn
584
  uvicorn.run(app, host="0.0.0.0", port=7860)
585
 
586
- print("🚀 SPACE 19 v11.0 STEEL — ГОТОВ!")
 
1
  # ============================================
2
+ # 👑 TOMIRIS SPACE 19 v12.0 «СТАЛЬ» — SOL/USD MASTER (УСИЛЕННЫЙ)
3
  # ============================================
4
+ import os, time, threading, warnings, json, asyncio, sqlite3, glob
5
  from typing import Dict, Any, Optional, List, Tuple
6
  import numpy as np, pandas as pd
7
+ import httpx
8
+ from datetime import datetime, timezone
9
  from collections import deque
10
  from fastapi import FastAPI, Query
11
  import logging
 
31
  HUB_URL = os.getenv("HUB_URL", "https://pro-3-tomiris-hub.hf.space")
32
  HUB_SECRET = os.getenv("HUB_SECRET", "TomyrisUltraSecret2026!")
33
 
34
+ STARTUP_SLEEP = int(os.getenv("STARTUP_SLEEP", "120"))
35
+ AUTO_REPORT_INTERVAL = int(os.getenv("AUTO_REPORT_INTERVAL", "300"))
36
  SOL_THRESHOLD = 0.52
37
 
38
+ CACHE_TTL = 300
39
  DATA_CACHE: Dict[str, Dict[str, Any]] = {}
40
  LAST_CONFIDENCE = 0.5
41
 
42
+ logger.info(f"🔗 Хаб: {HUB_URL} | Старт: {STARTUP_SLEEP}с | Интервал: {AUTO_REPORT_INTERVAL}с")
43
+
44
+ # ================= HTTP КЛИЕНТ =================
45
+ http_client = httpx.AsyncClient(timeout=15.0)
46
 
47
  def hub_headers():
48
  return {"X-Hub-Secret": HUB_SECRET, "Content-Type": "application/json"}
49
 
50
+ async def log_to_hub(event_type: str, message: str, details: dict = None):
51
+ try:
52
+ await http_client.post(
53
+ f"{HUB_URL}/log",
54
+ json={"space_id": str(SPACE_ID), "event_type": event_type, "message": message, "details": details or {}},
55
+ headers=hub_headers(), timeout=5
56
+ )
57
+ except: pass
58
+
59
+ # ================= ИСТОРИЯ ДЛЯ Z-SCORE =================
60
+ CONF_HISTORY = deque(maxlen=200)
61
+ SCORE_HISTORY = deque(maxlen=200)
62
+
63
+ def calculate_zscore(current: float, history: deque) -> float:
64
+ if len(history) < 10: return 0.0
65
+ arr = np.array(list(history))
66
+ mean, std = arr.mean(), arr.std()
67
+ if std == 0: return 0.0
68
+ return (current - mean) / std
69
+
70
+ # ================= SQLite =================
71
  DB_FILE = "sol_master.db"
72
 
73
  def init_db():
 
85
  signal TEXT NOT NULL,
86
  confidence REAL,
87
  regime TEXT,
88
+ models_used INTEGER,
89
+ adx REAL,
90
+ score REAL
91
  )''')
 
92
  for comp in ["model", "tf", "onchain", "derivatives"]:
93
  c.execute("INSERT OR IGNORE INTO component_perf (component, correct, total, sharpe) VALUES (?, 0, 1, 1.0)", (comp,))
94
  conn.commit()
 
107
  perf[row[0]] = {"correct": row[1], "total": row[2], "sharpe": row[3]}
108
  conn.close()
109
  except: pass
 
 
110
  for comp in ["model", "tf", "onchain", "derivatives"]:
111
  if comp not in perf:
112
  perf[comp] = {"correct": 0, "total": 1, "sharpe": 1.0}
 
126
  COMPONENT_PERF = load_component_perf()
127
 
128
  REGIME_WEIGHTS = {
129
+ "TREND": {"model": 0.65, "tf": 0.35},
130
+ "VOLATILE": {"model": 0.45, "tf": 0.55},
131
+ "RANGE": {"model": 0.55, "tf": 0.45}
132
  }
133
 
134
  # ================= ЗАГРУЗКА МОДЕЛЕЙ =================
135
+ MODELS: Dict[str, Optional[Any]] = {}
136
+
137
+ def load_all_models():
138
+ global MODELS
139
+ MODELS = {}
140
+
141
+ if HAS_JOBLIB:
142
+ # Стандартные модели
143
+ for fname, key in [("xgboost_sol_daily.joblib", "xgb_daily"),
144
+ ("xgboost_sol_4h.joblib", "xgb_4h"),
145
+ ("lgb_sol.joblib", "lgb")]:
146
+ if os.path.exists(fname):
147
+ try:
148
+ MODELS[key] = joblib.load(fname)
149
+ logger.info(f"✅ {fname} загружен")
150
+ except Exception as e:
151
+ logger.warning(f"⚠️ {fname}: {e}")
152
+
153
+ # Дополнительные .joblib файлы
154
+ for filepath in glob.glob("*.joblib"):
155
+ filename = os.path.basename(filepath)
156
+ if filename not in ["xgboost_sol_daily.joblib", "xgboost_sol_4h.joblib", "lgb_sol.joblib"]:
157
+ model_name = filename.replace(".joblib", "")
158
+ if "sol" in model_name.lower():
159
+ try:
160
+ MODELS[model_name] = joblib.load(filepath)
161
+ logger.info(f"✅ Доп. модель: {model_name}")
162
+ except: pass
163
+
164
+ logger.info(f"🧠 SOL моделей: {sum(1 for m in MODELS.values() if m is not None)}")
165
+
166
+ load_all_models()
167
+
168
+ # ================= ФИКСИРОВАННЫЙ ПОРЯДОК ПРИЗНАКОВ =================
169
  FEATURE_ORDER = [
170
  "price", "return_1h", "return_24h", "hurst_exponent",
171
  "volatility_1h", "high_low_ratio",
 
173
  "ema_50", "price_vs_ema_50",
174
  "macd", "macd_signal", "macd_hist",
175
  "rsi_14", "adx", "atr_14", "atr_pct",
176
+ "tvl", "tvl_change_24h", "dex_volume_24h", "dex_change_24h",
177
  "funding_rate", "open_interest",
178
  "funding_bullish", "funding_bearish",
179
  "is_weekend", "hour"
 
203
  except: return float(close.iloc[-1])
204
 
205
  def calculate_adx(df: pd.DataFrame, period: int = 14) -> float:
206
+ if df is None or len(df) < period * 2: return 20.0
207
+ high = df["high"].astype(float).values; low = df["low"].astype(float).values; close = df["close"].astype(float).values
208
+ dm_plus = np.zeros(len(high)); dm_minus = np.zeros(len(high)); tr = np.zeros(len(high))
 
 
 
 
 
 
 
 
 
209
  for i in range(1, len(high)):
 
210
  tr[i] = max(high[i] - low[i], abs(high[i] - close[i-1]), abs(low[i] - close[i-1]))
211
+ up_move = high[i] - high[i-1]; down_move = low[i-1] - low[i]
212
+ if up_move > down_move and up_move > 0: dm_plus[i] = up_move
213
+ if down_move > up_move and down_move > 0: dm_minus[i] = down_move
 
 
 
 
 
 
 
 
214
  atr = np.mean(tr[-period:]) if np.mean(tr[-period:]) > 0 else 0.001
215
  di_plus = 100 * np.mean(dm_plus[-period:]) / atr
216
  di_minus = 100 * np.mean(dm_minus[-period:]) / atr
 
217
  dx_sum = di_plus + di_minus
218
+ if dx_sum > 0: return float(abs(di_plus - di_minus) / dx_sum * 100)
 
 
219
  return 20.0
220
 
221
  def hurst_exponent(series, lags=20):
 
240
  if volatility > 0.04: return "VOLATILE"
241
  return "RANGE"
242
 
243
+ # ================= ЗАГРУЗКА ДАННЫХ =================
244
+ async def fetch_ohlc_hub(symbol, tf, limit=200):
245
  cache_key = f"hub_{symbol}_{tf}"
246
  if cache_key in DATA_CACHE and time.time() - DATA_CACHE[cache_key].get("timestamp", 0) < CACHE_TTL:
247
  return DATA_CACHE[cache_key]["df"]
248
 
249
  try:
250
+ r = await http_client.get(
 
251
  f"{HUB_URL}/candles",
252
  params={"symbol": symbol, "interval": tf, "timeframe": tf, "limit": limit},
253
+ timeout=30, headers=hub_headers()
 
254
  )
255
  if r.status_code == 200:
256
  candles = r.json().get("candles", [])
 
268
  logger.warning(f"Hub {symbol} {tf}: {e}")
269
  return None
270
 
271
+ async def fetch_solana_onchain():
 
 
 
272
  cache_key = "solana_onchain"
273
  if cache_key in DATA_CACHE and time.time() - DATA_CACHE[cache_key].get("timestamp", 0) < CACHE_TTL:
274
  return DATA_CACHE[cache_key]["data"]
275
 
276
  result = {"tvl": 0, "tvl_change_24h": 0, "dex_volume_24h": 0, "dex_change_24h": 0}
277
  try:
278
+ r = await http_client.get("https://api.llama.fi/v2/chains/solana", timeout=10)
279
  if r.status_code == 200:
280
  text = r.text.strip()
281
  if text:
 
285
  result['tvl'] = float(data)
286
  elif isinstance(data, dict):
287
  result['tvl'] = data.get('tvl', 0)
288
+ result['tvl_change_24h'] = data.get('change_1d', 0) or data.get('change_24h', 0)
289
  except: pass
290
+
291
+ # DEX объёмы
292
+ r2 = await http_client.get("https://api.llama.fi/overview/dexs/solana?excludeTotalDataChart=true", timeout=10)
293
+ if r2.status_code == 200:
294
+ dex_data = r2.json()
295
+ result['dex_volume_24h'] = dex_data.get('total24h', 0)
296
+ result['dex_change_24h'] = dex_data.get('change_1d', 0) or dex_data.get('dailyChange', 0)
297
  except: pass
298
 
299
  result['tvl_trend'] = 'UP' if result.get('tvl_change_24h', 0) > 0 else 'DOWN'
300
  DATA_CACHE[cache_key] = {"data": result, "timestamp": time.time()}
301
  return result
302
 
303
+ async def fetch_binance_sol():
304
  result = {}
305
  try:
306
+ r = await http_client.get("https://fapi.binance.com/fapi/v1/premiumIndex?symbol=SOLUSDT", timeout=10)
307
  if r.status_code == 200:
308
+ data = r.json()
309
+ if isinstance(data, list):
310
+ for item in data:
311
+ if item.get('symbol') == 'SOLUSDT':
312
+ result['funding_rate'] = float(item.get('lastFundingRate', 0))
313
+ break
314
+ elif isinstance(data, dict):
315
+ result['funding_rate'] = float(data.get('lastFundingRate', 0))
316
+
317
+ fr = result.get('funding_rate', 0)
318
+ if fr > 0.005: result['funding_signal'] = 'CAUTION_LONG'
319
+ elif fr > 0.001: result['funding_signal'] = 'BULLISH'
320
+ elif fr < -0.005: result['funding_signal'] = 'CAUTION_SHORT'
321
+ elif fr < -0.001: result['funding_signal'] = 'BEARISH'
322
+ else: result['funding_signal'] = 'NEUTRAL'
 
323
  except: pass
324
+
325
  try:
326
+ r = await http_client.get("https://fapi.binance.com/fapi/v1/openInterest?symbol=SOLUSDT", timeout=10)
327
  if r.status_code == 200:
328
  result['open_interest'] = float(r.json().get('openInterest', 0))
329
  except: pass
330
  return result
331
 
332
+ # ================= ПОСТРОЕНИЕ ПРИЗНАКОВ =================
333
  def build_sol_features(df, onchain_data=None, derivatives=None) -> Dict:
334
+ if df is None or len(df) < 20: return {}
 
335
 
336
+ close = df["close"].astype(float); high = df["high"].astype(float); low = df["low"].astype(float)
 
 
337
 
338
  features = {}
339
  features["price"] = safe_float(close.iloc[-1])
 
351
  features[f"price_vs_ema_{span}"] = safe_float(((close.iloc[-1] - ema_val) / ema_val) * 100) if ema_val != 0 else 0
352
 
353
  if len(close) >= 26:
354
+ ema12 = close.ewm(span=12, adjust=False).mean(); ema26 = close.ewm(span=26, adjust=False).mean()
355
+ macd = ema12 - ema26; signal = macd.ewm(span=9, adjust=False).mean()
356
+ features["macd"] = safe_float(macd.iloc[-1]); features["macd_signal"] = safe_float(signal.iloc[-1])
 
 
 
357
  features["macd_hist"] = features["macd"] - features["macd_signal"]
358
  else:
359
  features["macd"] = features["macd_signal"] = features["macd_hist"] = 0.0
360
 
361
  features["rsi_14"] = safe_rsi(close, 14) if len(close) >= 14 else 50.0
 
 
362
  features["adx"] = calculate_adx(df, 14)
363
 
364
  if len(close) >= 14:
 
367
  features["atr_14"] = safe_float(tr.rolling(14, min_periods=14).mean().iloc[-1])
368
  features["atr_pct"] = (features["atr_14"] / (close.iloc[-1] + 1e-10)) * 100
369
  else:
370
+ features["atr_14"] = close.iloc[-1] * 0.03; features["atr_pct"] = 3.0
 
371
 
372
  if onchain_data:
373
  features["tvl"] = onchain_data.get("tvl", 0)
374
+ features["tvl_change_24h"] = onchain_data.get("tvl_change_24h", 0)
375
  features["dex_volume_24h"] = onchain_data.get("dex_volume_24h", 0)
376
+ features["dex_change_24h"] = onchain_data.get("dex_change_24h", 0)
377
  else:
378
+ features["tvl"] = features["tvl_change_24h"] = features["dex_volume_24h"] = features["dex_change_24h"] = 0
379
 
380
  if derivatives:
381
  features["funding_rate"] = derivatives.get("funding_rate", 0)
 
387
  features["funding_rate"] = features["open_interest"] = 0
388
  features["funding_bullish"] = features["funding_bearish"] = 0
389
 
390
+ now = datetime.now(timezone.utc)
391
  features["is_weekend"] = 1 if now.weekday() >= 5 else 0
392
  features["hour"] = now.hour
393
 
 
394
  ordered = {}
395
  for key in FEATURE_ORDER:
396
  ordered[key] = features.get(key, 0.0)
 
398
  return ordered
399
 
400
  # ================= ОТПРАВКА СИГНАЛА =================
401
+ async def send_signal_to_hub(signal: str, confidence: float, features: Dict = None):
402
+ if features is None: features = {}
 
 
403
  payload = {
404
+ "space_id": SPACE_ID, "space_name": SPACE_NAME,
405
+ "symbol": SYMBOL, "signal": signal, "confidence": round(confidence, 4),
406
+ "features": features, "metadata": {"version": "12.0"},
407
+ "timestamp": datetime.now(timezone.utc).isoformat()
 
 
 
 
408
  }
 
409
  for attempt in range(3):
410
  try:
411
+ r = await http_client.post(f"{HUB_URL}/signals", json=payload, timeout=15, headers=hub_headers())
 
412
  if r.status_code == 200:
413
  logger.info(f"📤 {SYMBOL}: {signal} conf={confidence:.3f}")
414
  return True
415
+ await asyncio.sleep(2)
416
+ except Exception as e:
417
+ logger.warning(f"Попытка {attempt+1}: {e}")
418
+ await asyncio.sleep(2)
 
 
419
  return False
420
 
421
+ # ================= 🔥 СИГНАЛ =================
422
+ async def get_sol_signal():
423
  global LAST_CONFIDENCE
424
  start = time.time()
425
 
426
+ onchain_data = await fetch_solana_onchain()
427
+ binance_data = await fetch_binance_sol()
428
 
429
  all_features = {}
430
  for tf in TIMEFRAMES:
431
+ df = await fetch_ohlc_hub(SYMBOL, tf)
432
  if df is not None and len(df) >= 30:
433
  feats = build_sol_features(df, onchain_data, binance_data)
434
  if feats:
435
  all_features[tf] = feats
436
 
437
  if not all_features:
438
+ await send_signal_to_hub("WAIT", 0.0, {"reason": "no_data"})
439
  return None
440
 
441
  h1_features = all_features.get("1h", list(all_features.values())[0])
442
  price = h1_features.get("price", 0)
443
+ if price == 0: return None
 
444
 
445
  regime = detect_market_regime(h1_features)
446
+ adx_val = h1_features.get("adx", 20)
447
 
448
+ # 🔥 ML предсказание
449
  xgb_prob = 0.5
450
  models_used = 0
451
+ all_probs = []
452
+
453
  try:
454
  X = np.array([h1_features.get(f, 0.0) for f in FEATURE_ORDER], dtype=np.float64).reshape(1, -1)
455
  X = np.nan_to_num(X)
456
 
457
+ for mk, model in MODELS.items():
458
+ if model and hasattr(model, 'predict_proba'):
 
459
  try:
460
+ proba = model.predict_proba(X)[0]
461
+ prob = float(proba[1] if len(proba) > 1 else proba[0])
462
+ all_probs.append(prob)
463
  models_used += 1
464
  except: pass
465
 
466
+ if all_probs:
467
+ mean_prob = np.mean(all_probs)
468
+ weighted_probs = [p * (1.0 + abs(p - 0.5)) for p in all_probs]
469
+ xgb_prob = np.mean(weighted_probs) * 0.6 + mean_prob * 0.4
 
 
 
 
 
470
  except: pass
471
 
472
+ # 🔥 Мульти-ТФ консенсус
473
  confirmations, total_tf = 0, 0
474
  for tf, feats in all_features.items():
475
  total_tf += 1
476
  ema_score = feats.get("price_vs_ema_21", 0)
477
  rsi_val = feats.get("rsi_14", 50)
478
  macd_hist = feats.get("macd_hist", 0)
479
+ if ema_score > 0 and rsi_val > 50 and macd_hist > 0: confirmations += 1
480
+ elif ema_score < 0 and rsi_val < 50 and macd_hist < 0: confirmations -= 1
481
+
 
482
  tf_norm = ((confirmations / max(total_tf, 1)) + 1) / 2
483
 
484
+ # 🔥 On-chain скор
485
+ tvl_change = onchain_data.get("tvl_change_24h", 0)
486
+ dex_change = onchain_data.get("dex_change_24h", 0)
487
+ onchain_score = 0.5 + (tvl_change / 40) + (dex_change / 80)
488
+ onchain_score = max(0.1, min(0.9, onchain_score))
489
 
490
+ # 🔥 Деривативы скор
491
  fs = binance_data.get("funding_signal", "NEUTRAL")
492
+ if fs == "BULLISH": deriv_score = 0.70
493
+ elif fs == "BEARISH": deriv_score = 0.30
494
+ elif fs == "CAUTION_LONG": deriv_score = 0.45
495
+ elif fs == "CAUTION_SHORT": deriv_score = 0.55
496
+ else: deriv_score = 0.50
497
 
498
+ # 🔥 Взвешенная агрегация
499
  base = REGIME_WEIGHTS.get(regime, REGIME_WEIGHTS["RANGE"])
500
  model_perf = COMPONENT_PERF.get("model", {"correct": 0, "total": 1})
501
  tf_perf = COMPONENT_PERF.get("tf", {"correct": 0, "total": 1})
502
 
503
+ model_acc = model_perf["correct"] / max(model_perf["total"], 1)
504
+ tf_acc = tf_perf["correct"] / max(tf_perf["total"], 1)
 
 
 
 
 
 
 
505
 
506
+ model_w = base["model"] * max(model_acc, 0.3)
507
+ tf_w = base["tf"] * max(tf_acc, 0.3)
508
  remaining = 1.0 - (model_w + tf_w)
509
+ onchain_w = remaining * 0.55
510
+ deriv_w = remaining * 0.45
511
  total_w = model_w + tf_w + onchain_w + deriv_w
512
  if total_w > 0:
513
  model_w /= total_w; tf_w /= total_w; onchain_w /= total_w; deriv_w /= total_w
514
 
515
+ final_score = xgb_prob * model_w + tf_norm * tf_w + onchain_score * onchain_w + deriv_score * deriv_w
516
  confidence = smooth_confidence(final_score)
517
 
518
+ # 🔥 Z-score уверенности
519
+ CONF_HISTORY.append(confidence)
520
+ conf_z = calculate_zscore(confidence, CONF_HISTORY)
521
+ SCORE_HISTORY.append(final_score)
522
+ score_z = calculate_zscore(final_score, SCORE_HISTORY)
523
+
524
+ # 🔥 Адаптивный порог на основе ADX
525
+ if adx_val > 35:
526
+ adaptive_threshold = SOL_THRESHOLD - 0.04 # В тренде — ниже порог
527
+ elif adx_val > 25:
528
+ adaptive_threshold = SOL_THRESHOLD
529
+ else:
530
+ adaptive_threshold = SOL_THRESHOLD + 0.04 # В рендже — выше порог
531
+
532
+ if confidence > adaptive_threshold + 0.08:
533
  signal = "BUY"
534
+ elif confidence < adaptive_threshold - 0.08:
535
  signal = "SELL"
536
  else:
537
  signal = "WAIT"
538
 
539
+ # Усиление от z-score
540
+ if conf_z > 2.0 and signal == "BUY": confidence = min(0.95, confidence * 1.2)
541
+ elif conf_z < -2.0 and signal == "SELL": confidence = min(0.95, confidence * 1.2)
542
+
543
+ features_out = {
544
+ "ml_prob": round(xgb_prob, 4),
545
+ "tf_norm": round(tf_norm, 4),
546
+ "onchain_score": round(onchain_score, 4),
547
+ "deriv_score": round(deriv_score, 4),
548
+ "regime": regime,
549
+ "adx": round(adx_val, 1),
550
+ "models_used": models_used,
551
+ "conf_zscore": round(conf_z, 2)
552
+ }
553
+
554
+ await send_signal_to_hub(signal, confidence, features_out)
555
 
556
  # Логируем в SQLite
557
  try:
558
  conn = sqlite3.connect(DB_FILE)
559
  c = conn.cursor()
560
+ c.execute("INSERT INTO signals_log (timestamp, signal, confidence, regime, models_used, adx, score) VALUES (?, ?, ?, ?, ?, ?, ?)",
561
+ (datetime.now(timezone.utc).isoformat(), signal, confidence, regime, models_used, round(adx_val, 1), round(final_score, 4)))
562
  conn.commit()
563
  conn.close()
564
  except: pass
565
 
566
+ elapsed = int((time.time() - start) * 1000)
567
+ logger.info(f"🥉 SOL: {signal} conf={confidence:.3f} score={final_score:.3f} regime={regime} adx={adx_val:.1f} models={models_used} | {elapsed}ms")
568
+
569
+ return {"signal": signal, "confidence": confidence, "score": final_score, "regime": regime}
570
 
571
+ # ================= АВТО-ОТПРАВКА =================
572
+ async def auto_report():
573
  logger.info(f"⏳ Стартовый сон {STARTUP_SLEEP}с...")
574
+ await log_to_hub("STARTUP", f"SOL Master v12.0 запущен, жду {STARTUP_SLEEP}с")
575
+ await asyncio.sleep(STARTUP_SLEEP)
576
  logger.info("✅ SOL Master — начинаю авто-отправку!")
577
  while True:
578
+ await asyncio.sleep(AUTO_REPORT_INTERVAL)
579
  try:
580
+ await get_sol_signal()
581
  except Exception as e:
582
  logger.error(f"Ошибка: {e}")
583
 
584
+ # ================= FASTAPI =================
585
+ app = FastAPI(title="SOL Master v12.0 STEEL")
586
 
587
+ @app.on_event("startup")
588
+ async def startup():
589
+ asyncio.create_task(auto_report())
590
+ logger.info(f"🚀 Space 19 v12.0 | Hub: {HUB_URL} | Models: {sum(1 for m in MODELS.values() if m is not None)}")
591
 
592
  @app.get("/health")
593
  async def health():
594
+ return {"space_id": SPACE_ID, "status": "ok", "version": "12.0", "symbol": SYMBOL, "models": sum(1 for m in MODELS.values() if m is not None)}
595
 
596
  @app.head("/health")
597
+ async def health_head(): return {}
 
598
 
599
  @app.get("/consilium")
600
  async def consilium():
601
+ result = await get_sol_signal()
602
+ if result: return {"signal": result["signal"], "confidence": result["confidence"]}
 
603
  return {"signal": "WAIT", "confidence": 0.0}
604
 
605
  @app.get("/")
606
  async def root():
607
+ return {"name": "SOL Master v12.0 STEEL", "space_id": SPACE_ID, "hub": HUB_URL}
608
 
609
  if __name__ == "__main__":
610
  import uvicorn
611
  uvicorn.run(app, host="0.0.0.0", port=7860)
612
 
613
+ print("🚀 SPACE 19 v12.0 STEEL — ГОТОВ!")