tomirisai80 commited on
Commit
bf750ed
·
verified ·
1 Parent(s): 7983cf2

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +738 -0
app.py ADDED
@@ -0,0 +1,738 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================
2
+ # АВТО-УСТАНОВКА ПАКЕТОВ
3
+ # ============================================
4
+ import subprocess
5
+ import sys
6
+ import importlib
7
+
8
+ REQUIRED_PACKAGES = {
9
+ 'numpy': 'numpy',
10
+ 'pandas': 'pandas',
11
+ 'requests': 'requests'
12
+ }
13
+
14
+ for module_name, pip_name in REQUIRED_PACKAGES.items():
15
+ try:
16
+ importlib.import_module(module_name)
17
+ except ImportError:
18
+ print(f"📦 Устанавливаю {pip_name}...")
19
+ subprocess.check_call([sys.executable, "-m", "pip", "install", pip_name])
20
+ print(f"✅ {pip_name} установлен!")
21
+
22
+ # ============================================
23
+ # 👑 TOMIRIS SPACE 19 v1.0 — SOL/USD MASTER
24
+ # ============================================
25
+ # Первый из 12 новых Space'ов экосистемного контура.
26
+ # Специализируется ТОЛЬКО на Solana.
27
+ # Модели XGBoost/LightGBM, ончейн-метрики Solana,
28
+ # Pump.fun, DeFi Llama, экосистемные факторы.
29
+ # ============================================
30
+
31
+ import os
32
+ import time
33
+ import threading
34
+ import warnings
35
+ import json
36
+ import asyncio
37
+ from typing import Dict, Any, Optional, List, Tuple
38
+ import numpy as np
39
+ import pandas as pd
40
+ import requests
41
+ from datetime import datetime, timedelta
42
+ from collections import deque
43
+ from fastapi import FastAPI, Query
44
+ warnings.filterwarnings('ignore')
45
+
46
+ # ================= БЕЗОПАСНЫЙ ИМПОРТ =================
47
+ HAS_JOBLIB = False
48
+ HAS_FIREBASE = False
49
+ HAS_YFINANCE = False
50
+
51
+ try:
52
+ import joblib
53
+ HAS_JOBLIB = True
54
+ except:
55
+ print("⚠️ joblib не установлен")
56
+
57
+ try:
58
+ import firebase_admin
59
+ from firebase_admin import credentials, firestore
60
+ HAS_FIREBASE = True
61
+ except:
62
+ print("⚠️ firebase_admin не установлен")
63
+
64
+ try:
65
+ import yfinance as yf
66
+ HAS_YFINANCE = True
67
+ except:
68
+ print("⚠️ yfinance не установлен")
69
+
70
+ # ================= FIREBASE =================
71
+ db = None
72
+ if HAS_FIREBASE:
73
+ try:
74
+ cred = credentials.Certificate("firebase-key.json")
75
+ firebase_admin.initialize_app(cred)
76
+ db = firestore.client()
77
+ print("✅ Firebase подключен")
78
+ except Exception as e:
79
+ print(f"⚠️ Firebase: {e}")
80
+
81
+ # ================= URL'ы СМЕЖНЫХ SPACE'ов =================
82
+ SPACE_URLS: Dict[str, str] = {
83
+ "space_1_xau": "https://nuxotetotmailsvoboden-tomiris.hf.space",
84
+ "space_2_eth": "https://nuxotetotmailsvoboden-tomiris-falcon-ai.hf.space",
85
+ "space_9_onchain": "https://nuxotetotnicksvoboden-name3.hf.space",
86
+ "space_10_whales": "https://nuxotetotnicksvoboden-name4.hf.space",
87
+ "space_17_hub": "https://tomiris-ai-name5-5.hf.space",
88
+ "space_18_arbiter": "https://tomiris-ai-name6-6.hf.space"
89
+ }
90
+
91
+ # ================= API КЛЮЧИ =================
92
+ TWELVE_KEYS: List[str] = [
93
+ "e3740c072fda4fe8b8539d40b07e445e",
94
+ "58e67e0008e24161ac9b1671b7c2d2d0"
95
+ ]
96
+
97
+ # ================= КОНФИГУРАЦИЯ =================
98
+ SYMBOL: str = "SOL/USD"
99
+ MT5_SYMBOL: str = "SOLUSD"
100
+ TIMEFRAMES: List[str] = ["15min", "1h", "4h"]
101
+
102
+ # Загружаем пороги из best_config.json если есть
103
+ try:
104
+ with open("best_config.json", "r") as f:
105
+ config = json.load(f)
106
+ SOL_THRESHOLD: float = config.get("sol", {}).get("threshold", 0.52)
107
+ TRADING_RULES: Dict[str, Any] = config.get("trading_rules", {})
108
+ except:
109
+ SOL_THRESHOLD: float = 0.52
110
+ TRADING_RULES: Dict[str, Any] = {
111
+ "sl_atr_multiplier": 2.0,
112
+ "tp_atr_multiplier": 4.0,
113
+ "max_spread_pct": 2.0,
114
+ "trailing_stop_activation": 0.008,
115
+ "trailing_stop_distance": 0.005,
116
+ "breakeven_at": 0.008
117
+ }
118
+
119
+ CACHE_TTL: int = 900
120
+ MT5_MAX_AGE_SEC: int = 300
121
+ HUB_CACHE_TTL: float = 5.0
122
+
123
+ # Адаптивные веса по режиму рынка
124
+ REGIME_WEIGHTS: Dict[str, Dict[str, float]] = {
125
+ "TREND": {"model": 0.70, "tf": 0.30},
126
+ "VOLATILE": {"model": 0.50, "tf": 0.50},
127
+ "CONGESTED": {"model": 0.45, "tf": 0.55},
128
+ "RANGE": {"model": 0.60, "tf": 0.40}
129
+ }
130
+
131
+ # Глобальные хранилища
132
+ FEATURES_STORE: Dict[str, Any] = {}
133
+ DATA_CACHE: Dict[str, Dict[str, Any]] = {}
134
+ PREDICTION_HISTORY = deque(maxlen=500)
135
+ HUB_CACHE: Dict[str, Any] = {"price": 0.0, "timestamp": 0.0, "fresh": False}
136
+ CIRCUIT_BREAKERS: Dict[str, Dict[str, int]] = {}
137
+ LAST_CONFIDENCE: float = 0.5
138
+
139
+ # Yahoo Finance маппинг
140
+ YAHOO_INTERVAL_MAP: Dict[str, str] = {
141
+ "15min": "15m",
142
+ "1h": "60m",
143
+ "4h": "4h"
144
+ }
145
+
146
+ # ================= ЗАГРУЗКА МОДЕЛЕЙ =================
147
+ print(f"🔥 SPACE 19 v1.0: Загрузка моделей для {SYMBOL}...")
148
+ MODELS: Dict[str, Optional[Any]] = {"xgb": None, "lgb": None}
149
+
150
+ if HAS_JOBLIB:
151
+ try:
152
+ MODELS["xgb"] = joblib.load("xgboost_sol_v3.joblib")
153
+ print("✅ XGBoost SOL загружен (v3)")
154
+ except Exception as e:
155
+ print(f"⚠️ XGBoost SOL: {e}")
156
+ try:
157
+ MODELS["xgb"] = joblib.load("xgboost_sol_v2.joblib")
158
+ print("✅ XGBoost SOL загружен (v2, fallback)")
159
+ except:
160
+ print("❌ XGBoost SOL не найден")
161
+
162
+ try:
163
+ MODELS["lgb"] = joblib.load("lgb_sol_v2.joblib")
164
+ print("✅ LightGBM SOL загружен (v2)")
165
+ except:
166
+ print("⚠️ LightGBM SOL не найден")
167
+ else:
168
+ print("⚠️ joblib не установлен — сигналы будут нейтральными")
169
+
170
+ # ================= УТИЛИТЫ =================
171
+ api_lock = threading.Lock()
172
+ twelve_counter: int = 0
173
+
174
+ def get_next_twelve_key() -> str:
175
+ global twelve_counter
176
+ with api_lock:
177
+ key = TWELVE_KEYS[twelve_counter % len(TWELVE_KEYS)]
178
+ twelve_counter += 1
179
+ return key
180
+
181
+ session = requests.Session()
182
+ session.headers.update({"User-Agent": "Tomiris-Space19-v1.0"})
183
+
184
+ def safe_float(value: Any, default: float = 0.0) -> float:
185
+ try:
186
+ if isinstance(value, (pd.Series, pd.DataFrame)):
187
+ val = value.iloc[-1] if len(value) > 0 else default
188
+ else:
189
+ val = value
190
+ result = float(val)
191
+ return result if not pd.isna(result) else default
192
+ except:
193
+ return default
194
+
195
+ def safe_rsi(close_series: pd.Series, period: int = 14) -> float:
196
+ try:
197
+ delta = close_series.diff()
198
+ gain = delta.clip(lower=0).rolling(period, min_periods=period).mean()
199
+ loss = (-delta.clip(upper=0)).rolling(period, min_periods=period).mean()
200
+ g_val, l_val = gain.iloc[-1], loss.iloc[-1]
201
+ if pd.notna(g_val) and pd.notna(l_val) and l_val > 0:
202
+ rs = g_val / l_val
203
+ return float(100 - (100 / (1 + rs)))
204
+ return 50.0
205
+ except:
206
+ return 50.0
207
+
208
+ def safe_ema(close_series: pd.Series, span: int) -> Tuple[Optional[pd.Series], float]:
209
+ try:
210
+ ema = close_series.ewm(span=span, adjust=False).mean()
211
+ return ema, safe_float(ema.iloc[-1])
212
+ except:
213
+ return None, 0.0
214
+
215
+ # ================= ФИЛЬТР КАЛМАНА =================
216
+ class KalmanFilter:
217
+ def __init__(self, process_noise: float = 1e-5, measurement_noise: float = 1e-4):
218
+ self.q = process_noise
219
+ self.r = measurement_noise
220
+ self.x = 0.0
221
+ self.p = 1.0
222
+
223
+ def update(self, z: float) -> float:
224
+ self.p = self.p + self.q
225
+ k = self.p / (self.p + self.r)
226
+ self.x = self.x + k * (z - self.x)
227
+ self.p = (1 - k) * self.p
228
+ return self.x
229
+
230
+ # ================= HURST EXPONENT =================
231
+ def hurst_exponent(series: pd.Series, lags: int = 20) -> float:
232
+ if len(series) < lags * 2:
233
+ return 0.5
234
+ lags_range = range(2, min(lags, len(series) // 2))
235
+ tau = [np.std(np.subtract(series.values[lag:], series.values[:-lag])) for lag in lags_range]
236
+ try:
237
+ poly = np.polyfit(np.log(list(lags_range)), np.log(tau), 1)
238
+ return float(poly[0] * 2.0)
239
+ except:
240
+ return 0.5
241
+
242
+ # ================= GOOGLE TRENDS =================
243
+ def fetch_google_trends_index(keyword: str) -> float:
244
+ try:
245
+ return 50.0
246
+ except:
247
+ return 50.0
248
+
249
+ # ================= DATA HUB =================
250
+ def get_mt5_price_from_hub() -> Dict[str, Any]:
251
+ global HUB_CACHE
252
+ if time.time() - HUB_CACHE.get("timestamp", 0) < HUB_CACHE_TTL:
253
+ if HUB_CACHE.get("fresh"):
254
+ return HUB_CACHE
255
+ try:
256
+ r = requests.get(f"{SPACE_URLS['space_17_hub']}/price/{SYMBOL}", timeout=3)
257
+ if r.status_code == 200:
258
+ data = r.json()
259
+ fresh = data.get("fresh", False)
260
+ mid = data.get("mid", 0)
261
+ if fresh and mid > 0:
262
+ HUB_CACHE = {
263
+ "price": mid, "bid": data.get("bid", 0), "ask": data.get("ask", 0),
264
+ "spread_pct": data.get("spread_pct", 0),
265
+ "timestamp": time.time(), "fresh": True, "source": "MT5_LIVE"
266
+ }
267
+ return HUB_CACHE
268
+ except:
269
+ pass
270
+ return {"price": 0.0, "timestamp": time.time(), "fresh": False, "source": "UNAVAILABLE"}
271
+
272
+ # ================= CIRCUIT BREAKER =================
273
+ def breaker_open(name: str) -> bool:
274
+ info = CIRCUIT_BREAKERS.get(name)
275
+ if not info: return False
276
+ if info["fails"] < 5: return False
277
+ if time.time() - info["last_fail"] > 300:
278
+ CIRCUIT_BREAKERS[name] = {"fails": 0, "last_fail": 0}
279
+ return False
280
+ return True
281
+
282
+ def breaker_fail(name: str) -> None:
283
+ info = CIRCUIT_BREAKERS.get(name, {"fails": 0, "last_fail": 0})
284
+ info["fails"] += 1
285
+ info["last_fail"] = time.time()
286
+ CIRCUIT_BREAKERS[name] = info
287
+
288
+ # ================= СГЛАЖИВАНИЕ =================
289
+ def smooth_confidence(current: float) -> float:
290
+ global LAST_CONFIDENCE
291
+ current = max(0.0, min(1.0, current))
292
+ smoothed = LAST_CONFIDENCE * 0.7 + current * 0.3
293
+ LAST_CONFIDENCE = smoothed
294
+ return smoothed
295
+
296
+ # ================= РЕЖИМ РЫНКА =================
297
+ def detect_market_regime(features: Dict[str, Any]) -> str:
298
+ adx = features.get("adx", 20.0)
299
+ volatility = features.get("volatility_1h", 0.0)
300
+ hurst = features.get("hurst_exponent", 0.5)
301
+
302
+ if adx > 30 and hurst > 0.55:
303
+ return "TREND"
304
+ if volatility > 0.04:
305
+ return "VOLATILE"
306
+ return "RANGE"
307
+
308
+ # ================= СТРЕСС-ТЕСТ =================
309
+ def stress_test(features: Dict[str, Any]) -> Optional[str]:
310
+ atr_pct = features.get("atr_pct", 3.0)
311
+ if atr_pct > 12.0:
312
+ return "WAIT"
313
+ vol_1h = features.get("volatility_1h", 0.0)
314
+ if vol_1h > 0.08:
315
+ return "WAIT"
316
+ return None
317
+
318
+ # ================= ЗАГРУЗКА ДАННЫХ =================
319
+ def fetch_twelvedata_sol(tf: str = "1h") -> Tuple[Optional[pd.DataFrame], Optional[str]]:
320
+ cache_key = f"td_sol_{tf}"
321
+ if cache_key in DATA_CACHE:
322
+ age = time.time() - DATA_CACHE[cache_key].get("timestamp", 0)
323
+ if age < CACHE_TTL:
324
+ return DATA_CACHE[cache_key]["df"], DATA_CACHE[cache_key]["source"]
325
+
326
+ for key_idx, key in enumerate(TWELVE_KEYS):
327
+ try:
328
+ url = f"https://api.twelvedata.com/time_series?symbol=SOL/USD&interval={tf}&outputsize=200&apikey={key}"
329
+ r = session.get(url, timeout=10)
330
+ if r.status_code == 200:
331
+ data = r.json()
332
+ if "values" in data:
333
+ df = pd.DataFrame(data["values"]).iloc[::-1].reset_index(drop=True)
334
+ for col in ["close", "high", "low", "open"]:
335
+ df[col] = pd.to_numeric(df[col], errors="coerce")
336
+ df["volume"] = pd.to_numeric(df.get("volume", 0), errors="coerce").fillna(0)
337
+ df = df.dropna(subset=["close", "high", "low", "open"])
338
+ if len(df) >= 30:
339
+ DATA_CACHE[cache_key] = {
340
+ "df": df,
341
+ "source": f"TwelveData-Key{key_idx+1}",
342
+ "timestamp": time.time()
343
+ }
344
+ return df, f"TwelveData-Key{key_idx+1}"
345
+ elif r.status_code == 429:
346
+ continue
347
+ except:
348
+ continue
349
+
350
+ if HAS_YFINANCE:
351
+ try:
352
+ yf_interval = YAHOO_INTERVAL_MAP.get(tf, "60m")
353
+ period_map = {"15min": "7d", "1h": "60d", "4h": "60d"}
354
+ yf_period = period_map.get(tf, "60d")
355
+ yf_data = yf.download("SOL-USD", period=yf_period, interval=yf_interval, progress=False)
356
+ if not yf_data.empty:
357
+ df = pd.DataFrame({
358
+ 'close': yf_data['Close'].values.flatten(),
359
+ 'high': yf_data['High'].values.flatten(),
360
+ 'low': yf_data['Low'].values.flatten(),
361
+ 'open': yf_data['Open'].values.flatten(),
362
+ 'volume': yf_data['Volume'].values.flatten()
363
+ }).dropna()
364
+ if len(df) >= 30:
365
+ DATA_CACHE[cache_key] = {"df": df, "source": "YahooFinance", "timestamp": time.time()}
366
+ return df, "YahooFinance"
367
+ except:
368
+ pass
369
+ return None, None
370
+
371
+ # ================= SOLANA ОНЧЕЙН-МЕТРИКИ =================
372
+ def fetch_solana_onchain() -> Dict[str, Any]:
373
+ """TVL, DEX volume, активные кошельки для Solana."""
374
+ cache_key = "solana_onchain"
375
+ if cache_key in DATA_CACHE:
376
+ age = time.time() - DATA_CACHE[cache_key].get("timestamp", 0)
377
+ if age < 300:
378
+ return DATA_CACHE[cache_key]["data"]
379
+
380
+ result: Dict[str, Any] = {}
381
+
382
+ # TVL
383
+ try:
384
+ r = requests.get("https://api.llama.fi/v2/tvl/solana", timeout=10)
385
+ if r.status_code == 200:
386
+ data = r.json()
387
+ result['tvl'] = data.get('tvl', 0)
388
+ result['tvl_change_24h'] = data.get('change_1d', 0)
389
+ result['tvl_trend'] = 'UP' if data.get('change_1d', 0) > 0 else 'DOWN'
390
+ except:
391
+ result['tvl'] = 0
392
+ result['tvl_trend'] = 'STABLE'
393
+
394
+ # DEX volume
395
+ try:
396
+ r = requests.get(
397
+ "https://api.llama.fi/overview/dexs/solana?excludeTotalDataChart=true&excludeTotalDataChartBreakdown=true",
398
+ timeout=10
399
+ )
400
+ if r.status_code == 200:
401
+ data = r.json()
402
+ result['dex_volume_24h'] = data.get('total24h', 0)
403
+ result['dex_change_24h'] = data.get('change_1d', 0)
404
+ except:
405
+ result['dex_volume_24h'] = 0
406
+
407
+ # Активные кошельки
408
+ try:
409
+ r = requests.get(
410
+ "https://api.llama.fi/overview/Solana?excludeTotalDataChart=true&excludeTotalDataChartBreakdown=true",
411
+ timeout=10
412
+ )
413
+ if r.status_code == 200:
414
+ data = r.json()
415
+ result['active_users'] = data.get('activeUsers', 0)
416
+ except:
417
+ result['active_users'] = 0
418
+
419
+ DATA_CACHE[cache_key] = {"data": result, "timestamp": time.time()}
420
+ return result
421
+
422
+ def fetch_pump_fun_activity() -> Dict[str, Any]:
423
+ """Активность Pump.fun через CoinGecko."""
424
+ try:
425
+ r = requests.get(
426
+ "https://api.coingecko.com/api/v3/coins/solana?community_data=true&developer_data=true",
427
+ timeout=10
428
+ )
429
+ if r.status_code == 200:
430
+ data = r.json()
431
+ community = data.get('community_data', {})
432
+ developer = data.get('developer_data', {})
433
+ return {
434
+ 'reddit_activity': community.get('reddit_average_posts_48h', 0),
435
+ 'developer_score': developer.get('developer_score', 0),
436
+ 'ecosystem_activity': 'HIGH' if developer.get('developer_score', 0) > 80 else 'MODERATE'
437
+ }
438
+ except:
439
+ pass
440
+ return {'ecosystem_activity': 'MODERATE'}
441
+
442
+ def fetch_coingecko_sol() -> Dict[str, Any]:
443
+ try:
444
+ r = session.get(
445
+ "https://api.coingecko.com/api/v3/coins/solana?localization=false&tickers=false&community_data=false&developer_data=false",
446
+ timeout=10
447
+ )
448
+ if r.status_code == 200:
449
+ md = r.json().get('market_data', {})
450
+ return {
451
+ 'market_cap': md.get('market_cap', {}).get('usd', 0),
452
+ 'total_volume': md.get('total_volume', {}).get('usd', 0),
453
+ 'price_change_24h': md.get('price_change_percentage_24h', 0)
454
+ }
455
+ except:
456
+ pass
457
+ return {'market_cap': 0, 'total_volume': 0, 'price_change_24h': 0}
458
+
459
+ def fetch_binance_sol() -> Dict[str, Any]:
460
+ """Funding Rate и Open Interest для SOLUSDT."""
461
+ result: Dict[str, Any] = {}
462
+ try:
463
+ r = requests.get("https://fapi.binance.com/fapi/v1/premiumIndex", timeout=10)
464
+ if r.status_code == 200:
465
+ for item in r.json():
466
+ if item.get('symbol') == 'SOLUSDT':
467
+ fr = float(item.get('lastFundingRate', 0))
468
+ result['funding_rate'] = fr
469
+ result['funding_signal'] = 'BEARISH' if fr > 0.001 else 'BULLISH' if fr < -0.001 else 'NEUTRAL'
470
+ except:
471
+ result['funding_rate'] = 0
472
+
473
+ try:
474
+ r = requests.get("https://fapi.binance.com/fapi/v1/openInterest?symbol=SOLUSDT", timeout=10)
475
+ if r.status_code == 200:
476
+ result['open_interest'] = float(r.json().get('openInterest', 0))
477
+ except:
478
+ result['open_interest'] = 0
479
+
480
+ return result
481
+
482
+ # ================= ПОСТРОЕНИЕ ПРИЗНАКОВ =================
483
+ def build_features_from_mt5(mt5_features: Dict[str, Any]) -> Dict[str, Any]:
484
+ features: Dict[str, Any] = {}
485
+ for k, v in mt5_features.items():
486
+ if isinstance(v, (int, float, np.floating, np.integer)):
487
+ features[k] = float(v)
488
+ elif isinstance(v, np.bool_):
489
+ features[k] = bool(v)
490
+ else:
491
+ features[k] = v
492
+ while len(features) < 200:
493
+ features[f"mt5_pad_{len(features)}"] = 0.0
494
+ return features
495
+
496
+ def build_sol_features(df: pd.DataFrame, onchain_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
497
+ if df is None or len(df) < 20:
498
+ return {}
499
+
500
+ try:
501
+ close = df["close"].astype(float)
502
+ high = df["high"].astype(float)
503
+ low = df["low"].astype(float)
504
+ open_p = df["open"].astype(float) if "open" in df.columns else close
505
+ volume = df["volume"].astype(float) if "volume" in df.columns else pd.Series([0.0]*len(df))
506
+ except:
507
+ return {}
508
+
509
+ features: Dict[str, Any] = {}
510
+
511
+ features["price"] = safe_float(close.iloc[-1])
512
+ features["return_1h"] = safe_float(close.pct_change(1).iloc[-1])
513
+ features["return_24h"] = safe_float(close.pct_change(24).iloc[-1]) if len(close) > 24 else 0.0
514
+
515
+ # Калман
516
+ kf = KalmanFilter()
517
+ kalman_close = [kf.update(x) for x in close.values]
518
+ features["kalman_price"] = kalman_close[-1]
519
+ features["kalman_diff"] = close.iloc[-1] - kalman_close[-1]
520
+
521
+ # Hurst
522
+ features["hurst_exponent"] = hurst_exponent(close)
523
+
524
+ # Волатильность
525
+ ret = close.pct_change()
526
+ features["volatility_1h"] = safe_float(ret.rolling(24, min_periods=24).std().iloc[-1]) if len(close) >= 24 else 0.0
527
+ features["high_low_ratio"] = safe_float(((high.iloc[-1] - low.iloc[-1]) / (close.iloc[-1] + 1e-10)) * 100)
528
+
529
+ # EMA
530
+ for span in [9, 21, 50]:
531
+ if len(close) >= span:
532
+ _, ema_val = safe_ema(close, span)
533
+ if ema_val != 0:
534
+ features[f"ema_{span}"] = ema_val
535
+ features[f"price_vs_ema_{span}"] = safe_float(((close.iloc[-1] - ema_val) / ema_val) * 100)
536
+
537
+ # MACD
538
+ if len(close) >= 26:
539
+ try:
540
+ ema12 = close.ewm(span=12, adjust=False).mean()
541
+ ema26 = close.ewm(span=26, adjust=False).mean()
542
+ macd = ema12 - ema26
543
+ signal = macd.ewm(span=9, adjust=False).mean()
544
+ features["macd"] = safe_float(macd.iloc[-1])
545
+ features["macd_signal"] = safe_float(signal.iloc[-1])
546
+ features["macd_hist"] = features["macd"] - features["macd_signal"]
547
+ except:
548
+ pass
549
+
550
+ # RSI
551
+ features["rsi_14"] = safe_rsi(close, 14) if len(close) >= 14 else 50.0
552
+
553
+ # ATR
554
+ if len(close) >= 14:
555
+ try:
556
+ prev_close = close.shift(1)
557
+ tr = pd.DataFrame({
558
+ "tr1": high - low,
559
+ "tr2": (high - prev_close).abs(),
560
+ "tr3": (low - prev_close).abs()
561
+ }).max(axis=1)
562
+ features["atr_14"] = safe_float(tr.rolling(14, min_periods=14).mean().iloc[-1])
563
+ features["atr_pct"] = (features["atr_14"] / (close.iloc[-1] + 1e-10)) * 100
564
+ except:
565
+ features["atr_14"] = close.iloc[-1] * 0.02
566
+
567
+ # Ончейн
568
+ if onchain_data:
569
+ features["tvl"] = onchain_data.get("tvl", 0)
570
+ features["tvl_trend"] = 1 if onchain_data.get("tvl_trend") == "UP" else -1
571
+ features["dex_volume_24h"] = onchain_data.get("dex_volume_24h", 0)
572
+ features["active_users"] = onchain_data.get("active_users", 0)
573
+
574
+ # Альтернативные индексы
575
+ for kw in ["solana", "memecoin", "pump_fun", "firedancer"]:
576
+ features[f"trends_{kw}"] = fetch_google_trends_index(kw)
577
+
578
+ # Время
579
+ now = datetime.utcnow()
580
+ features["is_weekend"] = 1 if now.weekday() >= 5 else 0
581
+ features["hour"] = now.hour
582
+
583
+ while len(features) < 200:
584
+ features[f"pad_{len(features)}"] = 0.0
585
+
586
+ return features
587
+
588
+ # ================= МУЛЬТИ-ТФ =================
589
+ def get_multi_tf_features(onchain_data: Dict[str, Any]) -> Tuple[Dict[str, Dict[str, Any]], List[str]]:
590
+ all_features: Dict[str, Dict[str, Any]] = {}
591
+ sources: List[str] = []
592
+ for tf in TIMEFRAMES:
593
+ df, source = fetch_twelvedata_sol(tf)
594
+ if df is not None and len(df) >= 30:
595
+ feats = build_sol_features(df, onchain_data)
596
+ if feats:
597
+ all_features[tf] = feats
598
+ sources.append(source or "Unknown")
599
+ return all_features, sources
600
+
601
+ # ================= ГЛАВНЫЙ СИГНАЛ =================
602
+ def get_sol_signal() -> Optional[Dict[str, Any]]:
603
+ global LAST_CONFIDENCE
604
+ start_time = time.time()
605
+
606
+ # MT5 данные
607
+ mt5_features: Optional[Dict[str, Any]] = None
608
+ mt5_price: Optional[float] = None
609
+ data_source: str = "UNKNOWN"
610
+
611
+ fs = FEATURES_STORE.get(SYMBOL, {})
612
+ age = time.time() - fs.get("timestamp", 0)
613
+ if age < MT5_MAX_AGE_SEC:
614
+ mt5_features = fs.get("features", {})
615
+ mt5_price = fs.get("price")
616
+ data_source = "MT5"
617
+ print(f"📡 Используем MT5 данные (возраст {age:.0f}с)")
618
+
619
+ # Ончейн и рынок
620
+ onchain_data = fetch_solana_onchain()
621
+ pump_fun = fetch_pump_fun_activity()
622
+ coingecko = fetch_coingecko_sol()
623
+ binance = fetch_binance_sol()
624
+
625
+ mtf_features: Dict[str, Dict[str, Any]] = {}
626
+ sources: List[str] = []
627
+
628
+ if mt5_features and len(mt5_features) >= 50:
629
+ model_features = build_features_from_mt5(mt5_features)
630
+ model_features["tvl"] = onchain_data.get("tvl", 0)
631
+ model_features["tvl_trend"] = 1 if onchain_data.get("tvl_trend") == "UP" else -1
632
+ model_features["active_users"] = onchain_data.get("active_users", 0)
633
+ price = mt5_price or model_features.get("H1_price", model_features.get("price", 0))
634
+ sources = ["MT5"]
635
+ else:
636
+ print(" ⚠️ MT5 данные недоступны, перехожу на API...")
637
+ mtf_features, sources = get_multi_tf_features(onchain_data)
638
+ if not mtf_features:
639
+ print("❌ Нет данных")
640
+ return None
641
+ h1_features = mtf_features.get("1h", list(mtf_features.values())[0])
642
+ model_features = h1_features
643
+ price = h1_features.get("price", 0)
644
+ data_source = "+".join(sources) if sources else "API"
645
+
646
+ if price == 0:
647
+ return None
648
+
649
+ # Стресс-тест
650
+ stress = stress_test(model_features)
651
+ if stress == "WAIT":
652
+ print("🛑 СТРЕСС-ТЕСТ: рынок слишком опасен")
653
+ return {
654
+ "space": "space_19_sol_master",
655
+ "symbol": SYMBOL,
656
+ "signal": {"direction": "WAIT", "confidence": 0.0},
657
+ "reason": "stress_test_black_swan"
658
+ }
659
+
660
+ regime = detect_market_regime(model_features)
661
+ print(f"📊 Режим: {regime} | Цена: ${price:.2f} | TVL: ${onchain_data.get('tvl', 0)/1e9:.1f}B | Данные: {data_source}")
662
+
663
+ # Предсказание модели
664
+ xgb_prob = 0.5
665
+ if model_features and MODELS.get("xgb"):
666
+ try:
667
+ fv = list(model_features.values())[:200]
668
+ while len(fv) < 200:
669
+ fv.append(0.0)
670
+ X = np.nan_to_num(np.array(fv, dtype=np.float64).reshape(1, -1))
671
+ proba = MODELS["xgb"].predict_proba(X)[0]
672
+ xgb_prob = float(proba[1] if len(proba) > 1 else proba[0])
673
+ xgb_prob = max(0.0, min(1.0, xgb_prob))
674
+ except:
675
+ pass
676
+
677
+ # Ончейн-скор
678
+ onchain_score = 0.0
679
+ if onchain_data.get("tvl_trend") == "UP":
680
+ onchain_score += 0.1
681
+ if onchain_data.get("dex_change_24h", 0) > 10:
682
+ onchain_score += 0.05
683
+ if pump_fun.get("ecosystem_activity") == "HIGH":
684
+ onchain_score += 0.05
685
+ if binance.get("funding_signal") == "BULLISH":
686
+ onchain_score += 0.05
687
+ elif binance.get("funding_signal") == "BEARISH":
688
+ onchain_score -= 0.05
689
+
690
+ # Мульти-ТФ
691
+ if data_source == "MT5":
692
+ confirmations, total_tf = 0, 0
693
+ for tf_key in ["M15", "H1", "H4"]:
694
+ ema_key = f"{tf_key}_price_vs_ema_21"
695
+ if ema_key in model_features:
696
+ total_tf += 1
697
+ if model_features.get(ema_key, 0) > 0:
698
+ confirmations += 1
699
+ else:
700
+ confirmations -= 1
701
+ tf_score = confirmations / max(total_tf, 1)
702
+ tf_norm = (tf_score + 1) / 2
703
+ else:
704
+ confirmations, total_tf = 0, 0
705
+ for tf_key in ["15min", "1h", "4h"]:
706
+ tf_feats = mtf_features.get(tf_key, {})
707
+ if not tf_feats:
708
+ continue
709
+ total_tf += 1
710
+ ema_score = tf_feats.get("price_vs_ema_21", 0)
711
+ rsi_val = tf_feats.get("rsi_14", 50)
712
+ macd_hist = tf_feats.get("macd_hist", 0)
713
+ if ema_score > 0 and rsi_val > 50 and macd_hist > 0:
714
+ confirmations += 1
715
+ elif ema_score < 0 and rsi_val < 50 and macd_hist < 0:
716
+ confirmations -= 1
717
+ tf_score = confirmations / max(total_tf, 1)
718
+ tf_norm = (tf_score + 1) / 2
719
+
720
+ # Веса
721
+ w = REGIME_WEIGHTS.get(regime, REGIME_WEIGHTS["RANGE"])
722
+ final_score = (
723
+ xgb_prob * w["model"] * 0.50 +
724
+ tf_norm * w["tf"] * 0.20 +
725
+ (0.5 + onchain_score) * 0.30
726
+ )
727
+
728
+ confidence = smooth_confidence(final_score)
729
+
730
+ if confidence > SOL_THRESHOLD + 0.08:
731
+ direction = "LONG"
732
+ elif confidence < SOL_THRESHOLD - 0.08:
733
+ direction = "SHORT"
734
+ else:
735
+ direction = "WAIT"
736
+
737
+ # SL/TP (шире из-за волатильности SOL)
738
+ atr = model_features.get("at