Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,11 +1,11 @@
|
|
| 1 |
# ============================================
|
| 2 |
-
# 👑 TOMIRIS SPACE 19
|
| 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
|
| 8 |
-
from datetime import datetime,
|
| 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", "
|
| 35 |
-
AUTO_REPORT_INTERVAL =
|
| 36 |
SOL_THRESHOLD = 0.52
|
| 37 |
|
| 38 |
-
CACHE_TTL =
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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":
|
| 108 |
-
"VOLATILE":
|
| 109 |
-
"RANGE":
|
| 110 |
}
|
| 111 |
|
| 112 |
# ================= ЗАГРУЗКА МОДЕЛЕЙ =================
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 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", "
|
| 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 |
-
|
| 170 |
-
|
| 171 |
-
|
| 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 |
-
|
| 186 |
-
up_move
|
| 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
|
| 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 =
|
| 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 =
|
| 289 |
if r.status_code == 200:
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
break
|
| 306 |
except: pass
|
|
|
|
| 307 |
try:
|
| 308 |
-
r =
|
| 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 =
|
| 341 |
-
macd =
|
| 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["
|
| 366 |
features["dex_volume_24h"] = onchain_data.get("dex_volume_24h", 0)
|
|
|
|
| 367 |
else:
|
| 368 |
-
features["tvl"] = features["
|
| 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.
|
| 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 |
-
"
|
| 399 |
-
"
|
| 400 |
-
"
|
| 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 |
-
|
| 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 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 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 =
|
| 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 |
-
|
| 457 |
-
|
| 458 |
-
if MODELS.get(mk):
|
| 459 |
try:
|
| 460 |
-
proba =
|
| 461 |
-
|
|
|
|
| 462 |
models_used += 1
|
| 463 |
except: pass
|
| 464 |
|
| 465 |
-
if
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 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 |
-
|
| 485 |
-
|
| 486 |
-
confirmations -= 1
|
| 487 |
tf_norm = ((confirmations / max(total_tf, 1)) + 1) / 2
|
| 488 |
|
| 489 |
-
#
|
| 490 |
-
|
| 491 |
-
|
|
|
|
|
|
|
| 492 |
|
| 493 |
-
|
| 494 |
fs = binance_data.get("funding_signal", "NEUTRAL")
|
| 495 |
-
if fs == "BULLISH":
|
| 496 |
-
elif fs == "BEARISH":
|
| 497 |
-
elif fs == "CAUTION_LONG":
|
| 498 |
-
elif fs == "CAUTION_SHORT":
|
|
|
|
| 499 |
|
| 500 |
-
# 🔥
|
| 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 |
-
|
| 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.
|
| 517 |
-
deriv_w = remaining * 0.
|
| 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 +
|
| 523 |
confidence = smooth_confidence(final_score)
|
| 524 |
|
| 525 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 526 |
signal = "BUY"
|
| 527 |
-
elif confidence <
|
| 528 |
signal = "SELL"
|
| 529 |
else:
|
| 530 |
signal = "WAIT"
|
| 531 |
|
| 532 |
-
|
| 533 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 546 |
-
|
|
|
|
|
|
|
| 547 |
|
| 548 |
-
|
|
|
|
| 549 |
logger.info(f"⏳ Стартовый сон {STARTUP_SLEEP}с...")
|
| 550 |
-
|
|
|
|
| 551 |
logger.info("✅ SOL Master — начинаю авто-отправку!")
|
| 552 |
while True:
|
| 553 |
-
|
| 554 |
try:
|
| 555 |
-
get_sol_signal()
|
| 556 |
except Exception as e:
|
| 557 |
logger.error(f"Ошибка: {e}")
|
| 558 |
|
| 559 |
-
|
|
|
|
| 560 |
|
| 561 |
-
app
|
|
|
|
|
|
|
|
|
|
| 562 |
|
| 563 |
@app.get("/health")
|
| 564 |
async def health():
|
| 565 |
-
return {"space_id": SPACE_ID, "status": "ok", "version": "
|
| 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
|
| 581 |
|
| 582 |
if __name__ == "__main__":
|
| 583 |
import uvicorn
|
| 584 |
uvicorn.run(app, host="0.0.0.0", port=7860)
|
| 585 |
|
| 586 |
-
print("🚀 SPACE 19
|
|
|
|
| 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 — ГОТОВ!")
|