Spaces:
Sleeping
Sleeping
| # ============================================ | |
| # 👑 TOMIRIS SPACE 19 v12.0 «СТАЛЬ» — SOL/USD MASTER (УСИЛЕННЫЙ) | |
| # ============================================ | |
| import os, time, threading, warnings, json, asyncio, sqlite3, glob | |
| from typing import Dict, Any, Optional, List, Tuple | |
| import numpy as np, pandas as pd | |
| import httpx | |
| from datetime import datetime, timezone | |
| from collections import deque | |
| from fastapi import FastAPI, Query | |
| import logging | |
| warnings.filterwarnings('ignore') | |
| logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") | |
| logger = logging.getLogger("Space19_SOL_Master") | |
| # ================= БЕЗОПАСНЫЙ ИМПОРТ ================= | |
| HAS_JOBLIB = False | |
| try: | |
| import joblib | |
| HAS_JOBLIB = True | |
| except: | |
| logger.warning("⚠️ joblib не установлен") | |
| # ================= КОНФИГУРАЦИЯ ================= | |
| SPACE_ID = 19 | |
| SPACE_NAME = "SOL Master" | |
| SYMBOL = "SOL/USD" | |
| TIMEFRAMES = ["15min", "1h", "4h"] | |
| HUB_URL = "https://TOMI-HUB-HUB-FINAL.hf.space" | |
| HUB_SECRET = os.getenv("HUB_SECRET", "TomyrisUltraSecret2026!") | |
| STARTUP_SLEEP = int(os.getenv("STARTUP_SLEEP", "120")) | |
| AUTO_REPORT_INTERVAL = int(os.getenv("AUTO_REPORT_INTERVAL", "300")) | |
| SOL_THRESHOLD = 0.52 | |
| CACHE_TTL = 300 | |
| DATA_CACHE: Dict[str, Dict[str, Any]] = {} | |
| LAST_CONFIDENCE = 0.5 | |
| logger.info(f"🔗 Хаб: {HUB_URL} | Старт: {STARTUP_SLEEP}с | Интервал: {AUTO_REPORT_INTERVAL}с") | |
| # ================= HTTP КЛИЕНТ ================= | |
| http_client = httpx.AsyncClient(timeout=15.0) | |
| def hub_headers(): | |
| return {"X-Hub-Secret": HUB_SECRET, "Content-Type": "application/json"} | |
| async def log_to_hub(event_type: str, message: str, details: dict = None): | |
| try: | |
| await http_client.post( | |
| f"{HUB_URL}/log", | |
| json={"space_id": str(SPACE_ID), "event_type": event_type, "message": message, "details": details or {}}, | |
| headers=hub_headers(), timeout=5 | |
| ) | |
| except: pass | |
| # ================= ИСТОРИЯ ДЛЯ Z-SCORE ================= | |
| CONF_HISTORY = deque(maxlen=200) | |
| SCORE_HISTORY = deque(maxlen=200) | |
| def calculate_zscore(current: float, history: deque) -> float: | |
| if len(history) < 10: return 0.0 | |
| arr = np.array(list(history)) | |
| mean, std = arr.mean(), arr.std() | |
| if std == 0: return 0.0 | |
| return (current - mean) / std | |
| # ================= SQLite ================= | |
| DB_FILE = "sol_master.db" | |
| def init_db(): | |
| conn = sqlite3.connect(DB_FILE) | |
| c = conn.cursor() | |
| c.execute('''CREATE TABLE IF NOT EXISTS component_perf ( | |
| component TEXT PRIMARY KEY, | |
| correct INTEGER DEFAULT 0, | |
| total INTEGER DEFAULT 1, | |
| sharpe REAL DEFAULT 1.0 | |
| )''') | |
| c.execute('''CREATE TABLE IF NOT EXISTS signals_log ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| timestamp TEXT NOT NULL, | |
| signal TEXT NOT NULL, | |
| confidence REAL, | |
| regime TEXT, | |
| models_used INTEGER, | |
| adx REAL, | |
| score REAL | |
| )''') | |
| for comp in ["model", "tf", "onchain", "derivatives"]: | |
| c.execute("INSERT OR IGNORE INTO component_perf (component, correct, total, sharpe) VALUES (?, 0, 1, 1.0)", (comp,)) | |
| conn.commit() | |
| conn.close() | |
| logger.info("🗄️ SQLite база SOL Master инициализирована") | |
| init_db() | |
| def load_component_perf(): | |
| perf = {} | |
| try: | |
| conn = sqlite3.connect(DB_FILE) | |
| c = conn.cursor() | |
| c.execute("SELECT component, correct, total, sharpe FROM component_perf") | |
| for row in c.fetchall(): | |
| perf[row[0]] = {"correct": row[1], "total": row[2], "sharpe": row[3]} | |
| conn.close() | |
| except: pass | |
| for comp in ["model", "tf", "onchain", "derivatives"]: | |
| if comp not in perf: | |
| perf[comp] = {"correct": 0, "total": 1, "sharpe": 1.0} | |
| return perf | |
| def save_component_perf(perf: Dict): | |
| try: | |
| conn = sqlite3.connect(DB_FILE) | |
| c = conn.cursor() | |
| for comp, data in perf.items(): | |
| c.execute("UPDATE component_perf SET correct=?, total=?, sharpe=? WHERE component=?", | |
| (data["correct"], data["total"], data["sharpe"], comp)) | |
| conn.commit() | |
| conn.close() | |
| except: pass | |
| COMPONENT_PERF = load_component_perf() | |
| REGIME_WEIGHTS = { | |
| "TREND": {"model": 0.65, "tf": 0.35}, | |
| "VOLATILE": {"model": 0.45, "tf": 0.55}, | |
| "RANGE": {"model": 0.55, "tf": 0.45} | |
| } | |
| # ================= ЗАГРУЗКА МОДЕЛЕЙ ================= | |
| MODELS: Dict[str, Optional[Any]] = {} | |
| def load_all_models(): | |
| global MODELS | |
| MODELS = {} | |
| if HAS_JOBLIB: | |
| # Стандартные модели | |
| for fname, key in [("xgboost_sol_daily.joblib", "xgb_daily"), | |
| ("xgboost_sol_4h.joblib", "xgb_4h"), | |
| ("lgb_sol.joblib", "lgb")]: | |
| if os.path.exists(fname): | |
| try: | |
| MODELS[key] = joblib.load(fname) | |
| logger.info(f"✅ {fname} загружен") | |
| except Exception as e: | |
| logger.warning(f"⚠️ {fname}: {e}") | |
| # Дополнительные .joblib файлы | |
| for filepath in glob.glob("*.joblib"): | |
| filename = os.path.basename(filepath) | |
| if filename not in ["xgboost_sol_daily.joblib", "xgboost_sol_4h.joblib", "lgb_sol.joblib"]: | |
| model_name = filename.replace(".joblib", "") | |
| if "sol" in model_name.lower(): | |
| try: | |
| MODELS[model_name] = joblib.load(filepath) | |
| logger.info(f"✅ Доп. модель: {model_name}") | |
| except: pass | |
| logger.info(f"🧠 SOL моделей: {sum(1 for m in MODELS.values() if m is not None)}") | |
| load_all_models() | |
| # ================= ФИКСИРОВАННЫЙ ПОРЯДОК ПРИЗНАКОВ ================= | |
| FEATURE_ORDER = [ | |
| "price", "return_1h", "return_24h", "hurst_exponent", | |
| "volatility_1h", "high_low_ratio", | |
| "ema_9", "price_vs_ema_9", "ema_21", "price_vs_ema_21", | |
| "ema_50", "price_vs_ema_50", | |
| "macd", "macd_signal", "macd_hist", | |
| "rsi_14", "adx", "atr_14", "atr_pct", | |
| "tvl", "tvl_change_24h", "dex_volume_24h", "dex_change_24h", | |
| "funding_rate", "open_interest", | |
| "funding_bullish", "funding_bearish", | |
| "is_weekend", "hour" | |
| ] | |
| # ================= УТИЛИТЫ ================= | |
| def safe_float(value, default=0.0): | |
| try: | |
| if isinstance(value, (pd.Series, pd.DataFrame)): | |
| return float(value.iloc[-1]) if len(value) > 0 else default | |
| return float(value) if not pd.isna(float(value)) else default | |
| except: return default | |
| def safe_rsi(close, period=14): | |
| try: | |
| delta = close.diff() | |
| gain = delta.clip(lower=0).rolling(period, min_periods=period).mean() | |
| loss = (-delta.clip(upper=0)).rolling(period, min_periods=period).mean() | |
| g_val, l_val = gain.iloc[-1], loss.iloc[-1] | |
| if pd.notna(g_val) and pd.notna(l_val) and l_val > 0: | |
| return float(100 - (100 / (1 + g_val/l_val))) | |
| return 50.0 | |
| except: return 50.0 | |
| def safe_ema(close, span): | |
| try: return float(close.ewm(span=span, adjust=False).mean().iloc[-1]) | |
| except: return float(close.iloc[-1]) | |
| def calculate_adx(df: pd.DataFrame, period: int = 14) -> float: | |
| if df is None or len(df) < period * 2: return 20.0 | |
| high = df["high"].astype(float).values; low = df["low"].astype(float).values; close = df["close"].astype(float).values | |
| dm_plus = np.zeros(len(high)); dm_minus = np.zeros(len(high)); tr = np.zeros(len(high)) | |
| for i in range(1, len(high)): | |
| tr[i] = max(high[i] - low[i], abs(high[i] - close[i-1]), abs(low[i] - close[i-1])) | |
| up_move = high[i] - high[i-1]; down_move = low[i-1] - low[i] | |
| if up_move > down_move and up_move > 0: dm_plus[i] = up_move | |
| if down_move > up_move and down_move > 0: dm_minus[i] = down_move | |
| atr = np.mean(tr[-period:]) if np.mean(tr[-period:]) > 0 else 0.001 | |
| di_plus = 100 * np.mean(dm_plus[-period:]) / atr | |
| di_minus = 100 * np.mean(dm_minus[-period:]) / atr | |
| dx_sum = di_plus + di_minus | |
| if dx_sum > 0: return float(abs(di_plus - di_minus) / dx_sum * 100) | |
| return 20.0 | |
| def hurst_exponent(series, lags=20): | |
| if len(series) < lags * 2: return 0.5 | |
| lags_range = range(2, min(lags, len(series)//2)) | |
| tau = [np.std(np.subtract(series.values[lag:], series.values[:-lag])) for lag in lags_range] | |
| try: return float(np.polyfit(np.log(list(lags_range)), np.log(tau), 1)[0] * 2.0) | |
| except: return 0.5 | |
| def smooth_confidence(current): | |
| global LAST_CONFIDENCE | |
| current = max(0.0, min(1.0, current)) | |
| smoothed = LAST_CONFIDENCE * 0.7 + current * 0.3 | |
| LAST_CONFIDENCE = smoothed | |
| return smoothed | |
| def detect_market_regime(features): | |
| adx = features.get("adx", 20.0) | |
| volatility = features.get("volatility_1h", 0.0) | |
| hurst = features.get("hurst_exponent", 0.5) | |
| if adx > 30 and hurst > 0.55: return "TREND" | |
| if volatility > 0.04: return "VOLATILE" | |
| return "RANGE" | |
| # ================= ЗАГРУЗКА ДАННЫХ ================= | |
| async def fetch_ohlc_hub(symbol, tf, limit=200): | |
| cache_key = f"hub_{symbol}_{tf}" | |
| if cache_key in DATA_CACHE and time.time() - DATA_CACHE[cache_key].get("timestamp", 0) < CACHE_TTL: | |
| return DATA_CACHE[cache_key]["df"] | |
| try: | |
| r = await http_client.get( | |
| f"{HUB_URL}/candles", | |
| params={"symbol": symbol, "interval": tf, "timeframe": tf, "limit": limit}, | |
| timeout=30, headers=hub_headers() | |
| ) | |
| if r.status_code == 200: | |
| candles = r.json().get("candles", []) | |
| if candles: | |
| df = pd.DataFrame(candles) | |
| if "o" in df.columns: | |
| df.rename(columns={"o":"open","h":"high","l":"low","c":"close","v":"volume"}, inplace=True) | |
| for col in ["open","high","low","close"]: | |
| df[col] = pd.to_numeric(df[col], errors="coerce") | |
| df["volume"] = pd.to_numeric(df.get("volume", 0), errors="coerce").fillna(0) | |
| if len(df) >= 30: | |
| DATA_CACHE[cache_key] = {"df": df, "timestamp": time.time()} | |
| return df | |
| except Exception as e: | |
| logger.warning(f"Hub {symbol} {tf}: {e}") | |
| return None | |
| async def fetch_solana_onchain(): | |
| cache_key = "solana_onchain" | |
| if cache_key in DATA_CACHE and time.time() - DATA_CACHE[cache_key].get("timestamp", 0) < CACHE_TTL: | |
| return DATA_CACHE[cache_key]["data"] | |
| result = {"tvl": 0, "tvl_change_24h": 0, "dex_volume_24h": 0, "dex_change_24h": 0} | |
| try: | |
| r = await http_client.get("https://api.llama.fi/v2/chains/solana", timeout=10) | |
| if r.status_code == 200: | |
| text = r.text.strip() | |
| if text: | |
| try: | |
| data = r.json() | |
| if isinstance(data, (int, float)): | |
| result['tvl'] = float(data) | |
| elif isinstance(data, dict): | |
| result['tvl'] = data.get('tvl', 0) | |
| result['tvl_change_24h'] = data.get('change_1d', 0) or data.get('change_24h', 0) | |
| except: pass | |
| # DEX объёмы | |
| r2 = await http_client.get("https://api.llama.fi/overview/dexs/solana?excludeTotalDataChart=true", timeout=10) | |
| if r2.status_code == 200: | |
| dex_data = r2.json() | |
| result['dex_volume_24h'] = dex_data.get('total24h', 0) | |
| result['dex_change_24h'] = dex_data.get('change_1d', 0) or dex_data.get('dailyChange', 0) | |
| except: pass | |
| result['tvl_trend'] = 'UP' if result.get('tvl_change_24h', 0) > 0 else 'DOWN' | |
| DATA_CACHE[cache_key] = {"data": result, "timestamp": time.time()} | |
| return result | |
| async def fetch_binance_sol(): | |
| result = {} | |
| try: | |
| r = await http_client.get("https://fapi.binance.com/fapi/v1/premiumIndex?symbol=SOLUSDT", timeout=10) | |
| if r.status_code == 200: | |
| data = r.json() | |
| if isinstance(data, list): | |
| for item in data: | |
| if item.get('symbol') == 'SOLUSDT': | |
| result['funding_rate'] = float(item.get('lastFundingRate', 0)) | |
| break | |
| elif isinstance(data, dict): | |
| result['funding_rate'] = float(data.get('lastFundingRate', 0)) | |
| fr = result.get('funding_rate', 0) | |
| if fr > 0.005: result['funding_signal'] = 'CAUTION_LONG' | |
| elif fr > 0.001: result['funding_signal'] = 'BULLISH' | |
| elif fr < -0.005: result['funding_signal'] = 'CAUTION_SHORT' | |
| elif fr < -0.001: result['funding_signal'] = 'BEARISH' | |
| else: result['funding_signal'] = 'NEUTRAL' | |
| except: pass | |
| try: | |
| r = await http_client.get("https://fapi.binance.com/fapi/v1/openInterest?symbol=SOLUSDT", timeout=10) | |
| if r.status_code == 200: | |
| result['open_interest'] = float(r.json().get('openInterest', 0)) | |
| except: pass | |
| return result | |
| # ================= ПОСТРОЕНИЕ ПРИЗНАКОВ ================= | |
| def build_sol_features(df, onchain_data=None, derivatives=None) -> Dict: | |
| if df is None or len(df) < 20: return {} | |
| close = df["close"].astype(float); high = df["high"].astype(float); low = df["low"].astype(float) | |
| features = {} | |
| features["price"] = safe_float(close.iloc[-1]) | |
| features["return_1h"] = safe_float(close.pct_change(1).iloc[-1]) | |
| features["return_24h"] = safe_float(close.pct_change(24).iloc[-1]) if len(close) > 24 else 0.0 | |
| features["hurst_exponent"] = hurst_exponent(close) | |
| ret = close.pct_change() | |
| features["volatility_1h"] = safe_float(ret.rolling(24, min_periods=24).std().iloc[-1]) if len(close) >= 24 else 0.0 | |
| features["high_low_ratio"] = safe_float(((high.iloc[-1] - low.iloc[-1]) / (close.iloc[-1] + 1e-10)) * 100) | |
| for span in [9, 21, 50]: | |
| ema_val = safe_ema(close, span) if len(close) >= span else close.iloc[-1] | |
| features[f"ema_{span}"] = ema_val | |
| features[f"price_vs_ema_{span}"] = safe_float(((close.iloc[-1] - ema_val) / ema_val) * 100) if ema_val != 0 else 0 | |
| if len(close) >= 26: | |
| ema12 = close.ewm(span=12, adjust=False).mean(); ema26 = close.ewm(span=26, adjust=False).mean() | |
| macd = ema12 - ema26; signal = macd.ewm(span=9, adjust=False).mean() | |
| features["macd"] = safe_float(macd.iloc[-1]); features["macd_signal"] = safe_float(signal.iloc[-1]) | |
| features["macd_hist"] = features["macd"] - features["macd_signal"] | |
| else: | |
| features["macd"] = features["macd_signal"] = features["macd_hist"] = 0.0 | |
| features["rsi_14"] = safe_rsi(close, 14) if len(close) >= 14 else 50.0 | |
| features["adx"] = calculate_adx(df, 14) | |
| if len(close) >= 14: | |
| prev_close = close.shift(1) | |
| tr = pd.DataFrame({"tr1": high-low, "tr2": (high-prev_close).abs(), "tr3": (low-prev_close).abs()}).max(axis=1) | |
| features["atr_14"] = safe_float(tr.rolling(14, min_periods=14).mean().iloc[-1]) | |
| features["atr_pct"] = (features["atr_14"] / (close.iloc[-1] + 1e-10)) * 100 | |
| else: | |
| features["atr_14"] = close.iloc[-1] * 0.03; features["atr_pct"] = 3.0 | |
| if onchain_data: | |
| features["tvl"] = onchain_data.get("tvl", 0) | |
| features["tvl_change_24h"] = onchain_data.get("tvl_change_24h", 0) | |
| features["dex_volume_24h"] = onchain_data.get("dex_volume_24h", 0) | |
| features["dex_change_24h"] = onchain_data.get("dex_change_24h", 0) | |
| else: | |
| features["tvl"] = features["tvl_change_24h"] = features["dex_volume_24h"] = features["dex_change_24h"] = 0 | |
| if derivatives: | |
| features["funding_rate"] = derivatives.get("funding_rate", 0) | |
| features["open_interest"] = derivatives.get("open_interest", 0) | |
| fs = derivatives.get("funding_signal", "NEUTRAL") | |
| features["funding_bullish"] = 1 if fs == "BULLISH" else 0 | |
| features["funding_bearish"] = 1 if fs in ("CAUTION_LONG", "CAUTION_SHORT") else 0 | |
| else: | |
| features["funding_rate"] = features["open_interest"] = 0 | |
| features["funding_bullish"] = features["funding_bearish"] = 0 | |
| now = datetime.now(timezone.utc) | |
| features["is_weekend"] = 1 if now.weekday() >= 5 else 0 | |
| features["hour"] = now.hour | |
| ordered = {} | |
| for key in FEATURE_ORDER: | |
| ordered[key] = features.get(key, 0.0) | |
| return ordered | |
| # ================= ОТПРАВКА СИГНАЛА ================= | |
| async def send_signal_to_hub(signal: str, confidence: float, features: Dict = None): | |
| if features is None: features = {} | |
| payload = { | |
| "space_id": SPACE_ID, "space_name": SPACE_NAME, | |
| "symbol": SYMBOL, "signal": signal, "confidence": round(confidence, 4), | |
| "features": features, "metadata": {"version": "12.0"}, | |
| "timestamp": datetime.now(timezone.utc).isoformat() | |
| } | |
| for attempt in range(3): | |
| try: | |
| r = await http_client.post(f"{HUB_URL}/signals", json=payload, timeout=15, headers=hub_headers()) | |
| if r.status_code == 200: | |
| logger.info(f"📤 {SYMBOL}: {signal} conf={confidence:.3f}") | |
| return True | |
| await asyncio.sleep(2) | |
| except Exception as e: | |
| logger.warning(f"Попытка {attempt+1}: {e}") | |
| await asyncio.sleep(2) | |
| return False | |
| # ================= 🔥 СИГНАЛ ================= | |
| async def get_sol_signal(): | |
| global LAST_CONFIDENCE | |
| start = time.time() | |
| onchain_data = await fetch_solana_onchain() | |
| binance_data = await fetch_binance_sol() | |
| all_features = {} | |
| for tf in TIMEFRAMES: | |
| df = await fetch_ohlc_hub(SYMBOL, tf) | |
| if df is not None and len(df) >= 30: | |
| feats = build_sol_features(df, onchain_data, binance_data) | |
| if feats: | |
| all_features[tf] = feats | |
| if not all_features: | |
| await send_signal_to_hub("WAIT", 0.0, {"reason": "no_data"}) | |
| return None | |
| h1_features = all_features.get("1h", list(all_features.values())[0]) | |
| price = h1_features.get("price", 0) | |
| if price == 0: return None | |
| regime = detect_market_regime(h1_features) | |
| adx_val = h1_features.get("adx", 20) | |
| # 🔥 ML предсказание | |
| xgb_prob = 0.5 | |
| models_used = 0 | |
| all_probs = [] | |
| try: | |
| X = np.array([h1_features.get(f, 0.0) for f in FEATURE_ORDER], dtype=np.float64).reshape(1, -1) | |
| X = np.nan_to_num(X) | |
| for mk, model in MODELS.items(): | |
| if model and hasattr(model, 'predict_proba'): | |
| try: | |
| proba = model.predict_proba(X)[0] | |
| prob = float(proba[1] if len(proba) > 1 else proba[0]) | |
| all_probs.append(prob) | |
| models_used += 1 | |
| except: pass | |
| if all_probs: | |
| mean_prob = np.mean(all_probs) | |
| weighted_probs = [p * (1.0 + abs(p - 0.5)) for p in all_probs] | |
| xgb_prob = np.mean(weighted_probs) * 0.6 + mean_prob * 0.4 | |
| except: pass | |
| # 🔥 Мульти-ТФ консенсус | |
| confirmations, total_tf = 0, 0 | |
| for tf, feats in all_features.items(): | |
| total_tf += 1 | |
| ema_score = feats.get("price_vs_ema_21", 0) | |
| rsi_val = feats.get("rsi_14", 50) | |
| macd_hist = feats.get("macd_hist", 0) | |
| if ema_score > 0 and rsi_val > 50 and macd_hist > 0: confirmations += 1 | |
| elif ema_score < 0 and rsi_val < 50 and macd_hist < 0: confirmations -= 1 | |
| tf_norm = ((confirmations / max(total_tf, 1)) + 1) / 2 | |
| # 🔥 On-chain скор | |
| tvl_change = onchain_data.get("tvl_change_24h", 0) | |
| dex_change = onchain_data.get("dex_change_24h", 0) | |
| onchain_score = 0.5 + (tvl_change / 40) + (dex_change / 80) | |
| onchain_score = max(0.1, min(0.9, onchain_score)) | |
| # 🔥 Деривативы скор | |
| fs = binance_data.get("funding_signal", "NEUTRAL") | |
| if fs == "BULLISH": deriv_score = 0.70 | |
| elif fs == "BEARISH": deriv_score = 0.30 | |
| elif fs == "CAUTION_LONG": deriv_score = 0.45 | |
| elif fs == "CAUTION_SHORT": deriv_score = 0.55 | |
| else: deriv_score = 0.50 | |
| # 🔥 Взвешенная агрегация | |
| base = REGIME_WEIGHTS.get(regime, REGIME_WEIGHTS["RANGE"]) | |
| model_perf = COMPONENT_PERF.get("model", {"correct": 0, "total": 1}) | |
| tf_perf = COMPONENT_PERF.get("tf", {"correct": 0, "total": 1}) | |
| model_acc = model_perf["correct"] / max(model_perf["total"], 1) | |
| tf_acc = tf_perf["correct"] / max(tf_perf["total"], 1) | |
| model_w = base["model"] * max(model_acc, 0.3) | |
| tf_w = base["tf"] * max(tf_acc, 0.3) | |
| remaining = 1.0 - (model_w + tf_w) | |
| onchain_w = remaining * 0.55 | |
| deriv_w = remaining * 0.45 | |
| total_w = model_w + tf_w + onchain_w + deriv_w | |
| if total_w > 0: | |
| model_w /= total_w; tf_w /= total_w; onchain_w /= total_w; deriv_w /= total_w | |
| final_score = xgb_prob * model_w + tf_norm * tf_w + onchain_score * onchain_w + deriv_score * deriv_w | |
| confidence = smooth_confidence(final_score) | |
| # 🔥 Z-score уверенности | |
| CONF_HISTORY.append(confidence) | |
| conf_z = calculate_zscore(confidence, CONF_HISTORY) | |
| SCORE_HISTORY.append(final_score) | |
| score_z = calculate_zscore(final_score, SCORE_HISTORY) | |
| # 🔥 Адаптивный порог на основе ADX | |
| if adx_val > 35: | |
| adaptive_threshold = SOL_THRESHOLD - 0.04 # В тренде — ниже порог | |
| elif adx_val > 25: | |
| adaptive_threshold = SOL_THRESHOLD | |
| else: | |
| adaptive_threshold = SOL_THRESHOLD + 0.04 # В рендже — выше порог | |
| if confidence > adaptive_threshold + 0.08: | |
| signal = "BUY" | |
| elif confidence < adaptive_threshold - 0.08: | |
| signal = "SELL" | |
| else: | |
| signal = "WAIT" | |
| # Усиление от z-score | |
| if conf_z > 2.0 and signal == "BUY": confidence = min(0.95, confidence * 1.2) | |
| elif conf_z < -2.0 and signal == "SELL": confidence = min(0.95, confidence * 1.2) | |
| features_out = { | |
| "ml_prob": round(xgb_prob, 4), | |
| "tf_norm": round(tf_norm, 4), | |
| "onchain_score": round(onchain_score, 4), | |
| "deriv_score": round(deriv_score, 4), | |
| "regime": regime, | |
| "adx": round(adx_val, 1), | |
| "models_used": models_used, | |
| "conf_zscore": round(conf_z, 2) | |
| } | |
| await send_signal_to_hub(signal, confidence, features_out) | |
| # Логируем в SQLite | |
| try: | |
| conn = sqlite3.connect(DB_FILE) | |
| c = conn.cursor() | |
| c.execute("INSERT INTO signals_log (timestamp, signal, confidence, regime, models_used, adx, score) VALUES (?, ?, ?, ?, ?, ?, ?)", | |
| (datetime.now(timezone.utc).isoformat(), signal, confidence, regime, models_used, round(adx_val, 1), round(final_score, 4))) | |
| conn.commit() | |
| conn.close() | |
| except: pass | |
| elapsed = int((time.time() - start) * 1000) | |
| logger.info(f"🥉 SOL: {signal} conf={confidence:.3f} score={final_score:.3f} regime={regime} adx={adx_val:.1f} models={models_used} | {elapsed}ms") | |
| return {"signal": signal, "confidence": confidence, "score": final_score, "regime": regime} | |
| # ================= АВТО-ОТПРАВКА ================= | |
| async def auto_report(): | |
| logger.info(f"⏳ Стартовый сон {STARTUP_SLEEP}с...") | |
| await log_to_hub("STARTUP", f"SOL Master v12.0 запущен, жду {STARTUP_SLEEP}с") | |
| await asyncio.sleep(STARTUP_SLEEP) | |
| logger.info("✅ SOL Master — начинаю авто-отправку!") | |
| while True: | |
| await asyncio.sleep(AUTO_REPORT_INTERVAL) | |
| try: | |
| await get_sol_signal() | |
| except Exception as e: | |
| logger.error(f"Ошибка: {e}") | |
| # ================= FASTAPI ================= | |
| app = FastAPI(title="SOL Master v12.0 STEEL") | |
| async def startup(): | |
| asyncio.create_task(auto_report()) | |
| logger.info(f"🚀 Space 19 v12.0 | Hub: {HUB_URL} | Models: {sum(1 for m in MODELS.values() if m is not None)}") | |
| async def health(): | |
| 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)} | |
| async def health_head(): return {} | |
| async def consilium(): | |
| result = await get_sol_signal() | |
| if result: return {"signal": result["signal"], "confidence": result["confidence"]} | |
| return {"signal": "WAIT", "confidence": 0.0} | |
| async def root(): | |
| return {"name": "SOL Master v12.0 STEEL", "space_id": SPACE_ID, "hub": HUB_URL} | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run(app, host="0.0.0.0", port=7860) | |
| print("🚀 SPACE 19 v12.0 STEEL — ГОТОВ!") |