tomirisai80 commited on
Commit
e0ac224
·
verified ·
1 Parent(s): dd7f628

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +291 -309
app.py CHANGED
@@ -19,11 +19,11 @@ for module_name, pip_name in REQUIRED_PACKAGES.items():
19
  print(f"✅ {pip_name} установлен!")
20
 
21
  # ============================================
22
- # 👑 TOMIRIS SPACE 22 v1.0 — CROSS-ASSET SENTIMENT ENGINE
23
  # ============================================
24
- # Мониторит настроения рынка: Reddit, Google Trends, мемы,
25
- # панические атаки, эйфорию. Работает для XAU, ETH, SOL.
26
- # Контриан-индикатор: когда толпа паникует покупаем.
27
  # ============================================
28
 
29
  from fastapi import FastAPI, Query
@@ -34,367 +34,343 @@ import threading
34
  import numpy as np
35
  from datetime import datetime
36
  from collections import deque
37
- import xml.etree.ElementTree as ET
38
  import warnings
39
  warnings.filterwarnings('ignore')
40
 
41
  # ================= КОНФИГУРАЦИЯ =================
42
- SYMBOLS: List[str] = ["XAU/USD", "ETH/USD", "SOL/USD"]
43
 
44
  SPACE_18_ARBITER: str = "https://tomiris-ai-name6-6.hf.space"
45
 
46
- # Категории ключевых слов
47
- FEAR_KEYWORDS: Dict[str, List[str]] = {
48
- "XAU/USD": ["gold crash", "gold selloff", "gold bubble", "xau short"],
49
- "ETH/USD": ["ethereum crash", "eth sell", "defi hack", "eth dump"],
50
- "SOL/USD": ["solana crash", "sol dump", "solana outage", "sol hack"]
51
- }
52
-
53
- GREED_KEYWORDS: Dict[str, List[str]] = {
54
- "XAU/USD": ["gold moon", "gold to the moon", "buy gold now", "xau long"],
55
- "ETH/USD": ["ethereum moon", "eth to the moon", "eth pump", "buy eth"],
56
- "SOL/USD": ["solana moon", "sol to the moon", "sol pump", "buy sol"]
57
- }
58
-
59
- MEME_KEYWORDS: List[str] = ["meme coin", "pump", "dump", "wagmi", "ngmi", "to the moon", "wen lambo"]
60
 
61
  CACHE: Dict[str, Dict[str, Any]] = {}
62
  CACHE_TIMES: Dict[str, float] = {}
63
  FEATURES_STORE: Dict[str, Dict[str, Any]] = {}
64
  MT5_MAX_AGE_SEC: int = 300
65
- MEME_HISTORY: deque = deque(maxlen=100)
 
 
 
 
 
 
 
 
 
 
66
 
67
  def send_to_arbiter(signal_data: Dict[str, Any]) -> None:
68
  try:
69
  requests.post(
70
  f"{SPACE_18_ARBITER}/log_signal",
71
- json={'space': 'space_22_sentiment', 'symbol': 'XAU/USD', 'signal': signal_data.get('signals', {}).get('XAU/USD', {})},
72
  timeout=5
73
  )
74
  except:
75
  pass
76
 
77
- # ================= REDDIT СКАНЕР =================
78
- def fetch_reddit_sentiment(symbol: str) -> Dict[str, Any]:
79
- """Сканирует Reddit на упоминания."""
80
- cache_key = f"reddit_{symbol.replace('/', '_')}"
81
- if cache_key in CACHE and time.time() - CACHE_TIMES.get(cache_key, 0) < 600:
82
  return CACHE[cache_key]
83
 
84
  try:
85
- query = symbol.replace("/USD", "").lower()
86
- if "xau" in query:
87
- query = "gold"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
 
89
- headers = {'User-Agent': 'Mozilla/5.0 (compatible; TomirisSentiment/1.0)'}
90
- url = f"https://www.reddit.com/r/CryptoCurrency+wallstreetbets+investing/search.json?q={query}&sort=new&limit=20"
91
- r = requests.get(url, headers=headers, timeout=10)
 
 
92
 
93
- if r.status_code == 200:
94
- posts = r.json().get('data', {}).get('children', [])
95
- fear_count = 0
96
- greed_count = 0
97
- total_sentiment = 0.0
98
-
99
- for post in posts:
100
- data = post.get('data', {})
101
- title = data.get('title', '').lower()
102
- text = data.get('selftext', '')[:200].lower()
103
- full_text = title + ' ' + text
104
- ups = data.get('ups', 0)
105
- comments = data.get('num_comments', 0)
106
- viral_score = ups + comments * 2
107
-
108
- for kw in FEAR_KEYWORDS.get(symbol, []):
109
- if kw in full_text:
110
- fear_count += 1
111
- total_sentiment -= viral_score
112
- for kw in GREED_KEYWORDS.get(symbol, []):
113
- if kw in full_text:
114
- greed_count += 1
115
- total_sentiment += viral_score
116
- for kw in MEME_KEYWORDS:
117
- if kw in full_text:
118
- MEME_HISTORY.append({"keyword": kw, "symbol": symbol, "timestamp": time.time()})
119
-
120
- total = fear_count + greed_count
121
- fear_ratio = fear_count / total if total > 0 else 0.5
122
-
123
- if fear_ratio > 0.65:
124
- sentiment = "FEAR"
125
- signal = "BULLISH" # Покупаем на страхе
126
- elif fear_ratio > 0.50:
127
- sentiment = "SLIGHT_FEAR"
128
- signal = "SLIGHTLY_BULLISH"
129
- elif fear_ratio < 0.35:
130
- sentiment = "GREED"
131
- signal = "BEARISH" # Продаём на жадности
132
- elif fear_ratio < 0.50:
133
- sentiment = "SLIGHT_GREED"
134
- signal = "SLIGHTLY_BEARISH"
135
- else:
136
- sentiment = "NEUTRAL"
137
- signal = "NEUTRAL"
138
 
139
  result = {
140
- 'sentiment': sentiment,
141
- 'fear_ratio': round(fear_ratio, 4),
142
- 'fear_count': fear_count,
143
- 'greed_count': greed_count,
144
- 'total_posts': len(posts),
145
- 'market_signal': signal,
146
- 'source': 'Reddit'
147
  }
148
-
149
  CACHE[cache_key] = result
150
  CACHE_TIMES[cache_key] = time.time()
151
  return result
152
  except:
153
  pass
154
 
155
- return {'sentiment': 'NEUTRAL', 'fear_ratio': 0.5, 'market_signal': 'NEUTRAL', 'source': 'Reddit'}
156
 
157
- # ================= GOOGLE TRENDS =================
158
- def fetch_google_trends(symbol: str) -> Dict[str, Any]:
159
- """Оценка трендов через Google News RSS."""
160
- cache_key = f"trends_{symbol.replace('/', '_')}"
161
- if cache_key in CACHE and time.time() - CACHE_TIMES.get(cache_key, 0) < 900:
162
  return CACHE[cache_key]
163
 
164
  try:
165
- query = symbol.replace("/USD", "").lower()
166
- if "xau" in query:
167
- query = "gold price"
168
 
169
- url = f"https://news.google.com/rss/search?q={query}&hl=en-US&ceid=US:en"
170
- r = requests.get(url, timeout=10)
171
  if r.status_code == 200:
172
- root = ET.fromstring(r.content)
173
- items = root.findall("./channel/item")
174
- mention_count = len(items)
175
-
176
- if mention_count > 30:
177
- level = "EXTREME"
178
- elif mention_count > 20:
179
- level = "HIGH"
180
- elif mention_count > 10:
181
- level = "MODERATE"
182
- else:
183
- level = "LOW"
184
-
185
- # Высокий интерес = жадность для крипты, страх для золота
186
- if "xau" in symbol.lower():
187
- signal = "BULLISH" if level in ["HIGH", "EXTREME"] else "NEUTRAL"
188
- else:
189
- signal = "BEARISH" if level == "EXTREME" else "BULLISH" if level == "LOW" else "NEUTRAL"
190
 
191
  result = {
192
- 'mention_count': mention_count,
193
- 'level': level,
194
- 'market_signal': signal,
195
- 'source': 'Google Trends'
 
 
196
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
 
 
 
 
 
 
 
 
 
 
198
  CACHE[cache_key] = result
199
  CACHE_TIMES[cache_key] = time.time()
200
  return result
201
  except:
202
  pass
203
 
204
- return {'mention_count': 0, 'level': 'LOW', 'market_signal': 'NEUTRAL', 'source': 'Google Trends'}
205
-
206
- # ================= МЕМ-ДЕТЕКТОР =================
207
- def analyze_meme_activity() -> Dict[str, Any]:
208
- """Анализ мемной активности."""
209
- if not MEME_HISTORY:
210
- return {'meme_level': 'LOW', 'market_signal': 'NEUTRAL'}
211
-
212
- recent_memes = [m for m in MEME_HISTORY if time.time() - m.get('timestamp', 0) < 3600]
213
- meme_count = len(recent_memes)
214
-
215
- if meme_count > 20:
216
- level = "EXTREME"
217
- signal = "BEARISH" # Слишком много мемов = эйфория
218
- elif meme_count > 10:
219
- level = "HIGH"
220
- signal = "SLIGHTLY_BEARISH"
221
- elif meme_count > 5:
222
- level = "MODERATE"
223
- signal = "NEUTRAL"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  else:
225
- level = "LOW"
226
  signal = "NEUTRAL"
227
 
 
 
 
 
 
 
 
 
228
  return {
229
- 'meme_level': level,
230
- 'recent_memes': meme_count,
231
- 'market_signal': signal,
232
- 'source': 'Meme Detector'
 
 
 
 
 
 
233
  }
234
 
235
  # ================= АНАЛИЗ ИЗ MT5 =================
236
  def analyze_from_mt5(mt5_features: Dict[str, Any]) -> Optional[Dict[str, Any]]:
237
  try:
238
- score = 50.0
239
- now = datetime.utcnow()
240
- hour = now.hour
241
-
242
- if 13 <= hour < 22:
243
- score -= 5
244
- elif hour < 8:
245
- score += 5
246
-
247
- rsi = mt5_features.get('H1_rsi', 50)
248
- if isinstance(rsi, (int, float)):
249
- if rsi > 70:
250
- score -= 15
251
- elif rsi < 30:
252
- score += 15
253
-
254
- atr = mt5_features.get('H1_atr_pct', 1)
255
- if isinstance(atr, (int, float)) and atr > 2:
256
- score -= 10
257
-
258
- score = max(0, min(100, score))
259
-
260
- if score > 60:
261
- sentiment = "FEAR"
262
- direction = "LONG"
263
- elif score < 40:
264
- sentiment = "GREED"
265
- direction = "SHORT"
266
  else:
267
- sentiment = "NEUTRAL"
268
- direction = "WAIT"
269
 
270
  return {
271
- "sentiment": sentiment,
272
- "direction": direction,
273
- "confidence": round(abs(score - 50) / 50, 4),
274
- "sentiment_score": score,
275
  "source": "MT5"
276
  }
277
  except:
278
  return None
279
 
280
- # ================= ГЛАВНЫЙ АНАЛИЗ =================
281
- def analyze_sentiment(symbol: str) -> Dict[str, Any]:
282
- reddit = fetch_reddit_sentiment(symbol)
283
- trends = fetch_google_trends(symbol)
284
- meme = analyze_meme_activity()
285
-
286
- signals = []
287
- score = 50.0
288
-
289
- # Reddit
290
- if reddit.get('market_signal') == 'BULLISH':
291
- signals.append({"source": "Reddit", "signal": "BULLISH", "reason": f"Страх ({reddit.get('fear_ratio', 0):.2f})"})
292
- score += 15
293
- elif reddit.get('market_signal') == 'BEARISH':
294
- signals.append({"source": "Reddit", "signal": "BEARISH", "reason": "Жадность"})
295
- score -= 15
296
-
297
- # Google Trends
298
- if trends.get('market_signal') == 'BULLISH':
299
- signals.append({"source": "Trends", "signal": "BULLISH", "reason": f"Интерес: {trends.get('level')}"})
300
- score += 10
301
- elif trends.get('market_signal') == 'BEARISH':
302
- signals.append({"source": "Trends", "signal": "BEARISH", "reason": f"Хайп: {trends.get('level')}"})
303
- score -= 10
304
-
305
- # Meme
306
- if meme.get('market_signal') == 'BEARISH':
307
- signals.append({"source": "Meme", "signal": "BEARISH", "reason": f"Мемов: {meme.get('meme_level')}"})
308
- score -= 10
309
- elif meme.get('market_signal') == 'SLIGHTLY_BEARISH':
310
- signals.append({"source": "Meme", "signal": "CAUTION", "reason": "Много мемов"})
311
- score -= 5
312
-
313
- score = max(0, min(100, score))
314
-
315
- if score > 60:
316
- direction = "LONG"
317
- confidence = score / 100
318
- elif score < 40:
319
- direction = "SHORT"
320
- confidence = (100 - score) / 100
321
- else:
322
- direction = "WAIT"
323
- confidence = 0.0
324
-
325
- return {
326
- "sentiment_score": score,
327
- "direction": direction,
328
- "confidence": round(confidence, 4),
329
- "signals": signals,
330
- "metrics": {
331
- "reddit": reddit,
332
- "google_trends": trends,
333
- "meme_activity": meme
334
- }
335
- }
336
-
337
  # ================= ГЛАВНЫЙ СИГНАЛ =================
338
- def get_sentiment_signal() -> Dict[str, Any]:
339
  start = time.time()
340
 
341
  # Проверка MT5
342
- for symbol in SYMBOLS:
343
- if symbol in FEATURES_STORE:
344
- fs = FEATURES_STORE[symbol]
345
- age = time.time() - fs.get("timestamp", 0)
346
- if age < MT5_MAX_AGE_SEC:
347
- mt5_features = fs.get("features", {})
348
- if mt5_features:
349
- mt5_result = analyze_from_mt5(mt5_features)
350
- if mt5_result and mt5_result["confidence"] > 0.3:
351
- result = {
352
- "space": "space_22_sentiment",
353
- "timestamp": int(time.time()),
354
- "signals": {
355
- s: {"direction": mt5_result["direction"], "confidence": mt5_result["confidence"]}
356
- for s in SYMBOLS
357
- },
358
- "sentiment": mt5_result["sentiment"],
359
- "data_source": "MT5",
360
- "meta": {"latency_ms": int((time.time() - start) * 1000)}
361
- }
362
- send_to_arbiter(result)
363
- print(f"💬 SENTIMENT: {mt5_result['sentiment']} | {mt5_result['direction']} (MT5)")
364
- return result
365
-
366
- # Fallback: полный анализ
367
- signals_dict = {}
368
- for symbol in SYMBOLS:
369
- analysis = analyze_sentiment(symbol)
370
- signals_dict[symbol] = {
371
- "direction": analysis['direction'],
372
- "confidence": analysis['confidence'],
373
- "sentiment_score": analysis['sentiment_score']
374
- }
375
-
376
  latency = int((time.time() - start) * 1000)
377
 
378
- # Агрегируем (все три актива)
379
- xau = signals_dict.get("XAU/USD", {})
380
- eth = signals_dict.get("ETH/USD", {})
381
- sol = signals_dict.get("SOL/USD", {})
382
 
383
  result = {
384
- "space": "space_22_sentiment",
385
  "timestamp": int(time.time()),
386
- "signals": signals_dict,
387
- "sentiment_analysis": {
388
- "gold": analyze_sentiment("XAU/USD"),
389
- "eth": analyze_sentiment("ETH/USD"),
390
- "sol": analyze_sentiment("SOL/USD")
 
 
 
 
 
391
  },
392
- "data_source": "SOCIAL",
393
  "meta": {"latency_ms": latency}
394
  }
395
 
396
  send_to_arbiter(result)
397
- print(f"💬 SENTIMENT v1: XAU={xau.get('direction')} | ETH={eth.get('direction')} | SOL={sol.get('direction')}")
 
398
  return result
399
 
400
  # ================= KEEP-ALIVE =================
@@ -409,48 +385,54 @@ def keep_alive():
409
  threading.Thread(target=keep_alive, daemon=True).start()
410
 
411
  # ================= FASTAPI =================
412
- app = FastAPI(title="TOMIRIS SPACE 22 v1.0 — CROSS-ASSET SENTIMENT ENGINE")
413
 
414
  @app.get("/health")
415
  @app.head("/health")
416
  async def health():
417
  return {
418
- "space": "Space 22 - Cross-Asset Sentiment Engine v1.0",
419
  "status": "operational",
420
- "symbols": SYMBOLS,
421
- "sources": ["Reddit", "Google Trends", "Meme Detector"],
422
- "meme_history": len(MEME_HISTORY)
423
  }
424
 
425
  @app.get("/consilium")
426
- async def consilium(symbol: str = Query("XAU/USD")):
427
- return get_sentiment_signal()
 
 
428
 
429
- @app.get("/sentiment/{symbol}")
430
- async def sentiment(symbol: str):
431
- if symbol not in SYMBOLS:
432
  return {"error": f"Unsupported: {symbol}"}
433
- return analyze_sentiment(symbol)
434
 
435
- @app.get("/meme")
436
- async def meme():
437
- return analyze_meme_activity()
438
 
439
- @app.get("/reddit/{symbol}")
440
- async def reddit(symbol: str):
441
- if symbol not in SYMBOLS:
442
- return {"error": f"Unsupported: {symbol}"}
443
- return fetch_reddit_sentiment(symbol)
444
 
445
- @app.get("/trends/{symbol}")
446
- async def trends(symbol: str):
447
- if symbol not in SYMBOLS:
448
- return {"error": f"Unsupported: {symbol}"}
449
- return fetch_google_trends(symbol)
 
 
 
 
 
 
450
 
451
  @app.post("/features")
452
  async def receive_features(data: Dict[str, Any]):
453
- symbol = data.get("symbol", "XAU/USD")
454
  FEATURES_STORE[symbol] = {
455
  "features": data.get("features", {}),
456
  "price": data.get("price", 0.0),
@@ -459,6 +441,6 @@ async def receive_features(data: Dict[str, Any]):
459
  print(f"📥 MT5 {symbol}: {len(data.get('features', {}))} признаков")
460
  return {"status": "ok"}
461
 
462
- print(f"🚀 SPACE 22 v1.0 — CROSS-ASSET SENTIMENT ENGINE ЗАПУЩЕН!")
463
- print(f"💬 Сканирую: Reddit | Google Trends | Мемы | для XAU, ETH, SOL")
464
  print(f"✅ Готов к бою!")
 
19
  print(f"✅ {pip_name} установлен!")
20
 
21
  # ============================================
22
+ # 👑 TOMIRIS SPACE 23 v1.0 — ON-CHAIN ANOMALY DETECTOR
23
  # ============================================
24
+ # Детектирует аномалии на блокчейне:
25
+ # Китовые транзакции, приток/отток на биржи, скачки газа,
26
+ # нетипичная активность, сливы/пампы.
27
  # ============================================
28
 
29
  from fastapi import FastAPI, Query
 
34
  import numpy as np
35
  from datetime import datetime
36
  from collections import deque
 
37
  import warnings
38
  warnings.filterwarnings('ignore')
39
 
40
  # ================= КОНФИГУРАЦИЯ =================
41
+ TRACKED_SYMBOLS: List[str] = ["ETH/USD", "SOL/USD"]
42
 
43
  SPACE_18_ARBITER: str = "https://tomiris-ai-name6-6.hf.space"
44
 
45
+ # API ключи
46
+ ETHERSCAN_KEYS: List[str] = [
47
+ "TZGVD58I2HZ9G4BBD548KX11E4JFH4J5GX",
48
+ "NMK1GC2S8RKVQZ6YKV7CIVZGGAKS1XYWSI"
49
+ ]
 
 
 
 
 
 
 
 
 
50
 
51
  CACHE: Dict[str, Dict[str, Any]] = {}
52
  CACHE_TIMES: Dict[str, float] = {}
53
  FEATURES_STORE: Dict[str, Dict[str, Any]] = {}
54
  MT5_MAX_AGE_SEC: int = 300
55
+ ANOMALY_HISTORY: deque = deque(maxlen=200)
56
+
57
+ etherscan_counter: int = 0
58
+ api_lock = threading.Lock()
59
+
60
+ def get_next_etherscan_key() -> str:
61
+ global etherscan_counter
62
+ with api_lock:
63
+ key = ETHERSCAN_KEYS[etherscan_counter % len(ETHERSCAN_KEYS)]
64
+ etherscan_counter += 1
65
+ return key
66
 
67
  def send_to_arbiter(signal_data: Dict[str, Any]) -> None:
68
  try:
69
  requests.post(
70
  f"{SPACE_18_ARBITER}/log_signal",
71
+ json={'space': 'space_23_anomaly', 'symbol': 'ETH/USD', 'signal': signal_data.get('signal', {})},
72
  timeout=5
73
  )
74
  except:
75
  pass
76
 
77
+ # ================= ETHERSCAN =================
78
+ def fetch_gas_oracle() -> Dict[str, Any]:
79
+ cache_key = "gas_oracle"
80
+ if cache_key in CACHE and time.time() - CACHE_TIMES.get(cache_key, 0) < 60:
 
81
  return CACHE[cache_key]
82
 
83
  try:
84
+ key = get_next_etherscan_key()
85
+ r = requests.get(
86
+ f"https://api.etherscan.io/api?module=gastracker&action=gasoracle&apikey={key}",
87
+ timeout=10
88
+ )
89
+ if r.status_code == 200 and r.json().get('status') == '1':
90
+ result = r.json()['result']
91
+ avg = int(result['ProposeGasPrice'])
92
+ data = {
93
+ 'avg_gas': avg,
94
+ 'gas_level': 'LOW' if avg < 30 else 'NORMAL' if avg < 80 else 'HIGH' if avg < 150 else 'EXTREME',
95
+ 'is_anomaly': avg > 200
96
+ }
97
+ CACHE[cache_key] = data
98
+ CACHE_TIMES[cache_key] = time.time()
99
+ return data
100
+ except:
101
+ pass
102
+ return {'avg_gas': 45, 'gas_level': 'NORMAL', 'is_anomaly': False}
103
 
104
+ def fetch_large_transactions() -> Dict[str, Any]:
105
+ """Китовые транзакции (>100 ETH) через Etherscan."""
106
+ cache_key = "large_tx"
107
+ if cache_key in CACHE and time.time() - CACHE_TIMES.get(cache_key, 0) < 300:
108
+ return CACHE[cache_key]
109
 
110
+ try:
111
+ key = get_next_etherscan_key()
112
+ r = requests.get(
113
+ f"https://api.etherscan.io/api?module=account&action=txlist&address=0x00000000219ab540356cBB839Cbe05303d7705Fa&page=1&offset=10&sort=desc&apikey={key}",
114
+ timeout=10
115
+ )
116
+ if r.status_code == 200 and r.json().get('status') == '1':
117
+ txs = r.json()['result']
118
+ large_txs = []
119
+ for tx in txs:
120
+ value_eth = float(tx.get('value', 0)) / 1e18
121
+ if value_eth > 100:
122
+ large_txs.append({
123
+ 'hash': tx.get('hash', '')[:10],
124
+ 'value_eth': round(value_eth, 2),
125
+ 'from': tx.get('from', '')[:10],
126
+ 'to': tx.get('to', '')[:10]
127
+ })
128
+
129
+ is_anomaly = len(large_txs) > 3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
  result = {
132
+ 'large_transactions': large_txs[:5],
133
+ 'count': len(large_txs),
134
+ 'is_anomaly': is_anomaly,
135
+ 'signal': 'WHALE_ACTIVE' if is_anomaly else 'NORMAL'
 
 
 
136
  }
 
137
  CACHE[cache_key] = result
138
  CACHE_TIMES[cache_key] = time.time()
139
  return result
140
  except:
141
  pass
142
 
143
+ return {'large_transactions': [], 'count': 0, 'is_anomaly': False, 'signal': 'NORMAL'}
144
 
145
+ # ================= BINANCE ПРИТОКИ/ОТТОКИ =================
146
+ def fetch_exchange_flows(symbol: str = "ETH") -> Dict[str, Any]:
147
+ """Приток/отток на биржи (упрощённо через Binance данные)."""
148
+ cache_key = f"exchange_flows_{symbol}"
149
+ if cache_key in CACHE and time.time() - CACHE_TIMES.get(cache_key, 0) < 300:
150
  return CACHE[cache_key]
151
 
152
  try:
153
+ binance_symbol = "ETHUSDT" if symbol == "ETH" else "SOLUSDT"
 
 
154
 
155
+ # Оцениваем через large trades на Binance
156
+ r = requests.get(f"https://fapi.binance.com/fapi/v1/trades?symbol={binance_symbol}&limit=50", timeout=10)
157
  if r.status_code == 200:
158
+ trades = r.json()
159
+ large_trades = [t for t in trades if float(t.get('qty', 0)) > 100]
160
+ buy_large = sum(1 for t in large_trades if t.get('isBuyerMaker') == False)
161
+ sell_large = sum(1 for t in large_trades if t.get('isBuyerMaker') == True)
162
+
163
+ net_flow = buy_large - sell_large
164
+ is_anomaly = abs(net_flow) > 10
 
 
 
 
 
 
 
 
 
 
 
165
 
166
  result = {
167
+ 'symbol': symbol,
168
+ 'large_buys': buy_large,
169
+ 'large_sells': sell_large,
170
+ 'net_flow': net_flow,
171
+ 'signal': 'INFLOW' if net_flow > 10 else 'OUTFLOW' if net_flow < -10 else 'BALANCED',
172
+ 'is_anomaly': is_anomaly
173
  }
174
+ CACHE[cache_key] = result
175
+ CACHE_TIMES[cache_key] = time.time()
176
+ return result
177
+ except:
178
+ pass
179
+
180
+ return {'signal': 'BALANCED', 'is_anomaly': False}
181
+
182
+ # ================= АКТИВНЫЕ АДРЕСА (через DeFiLlama) =================
183
+ def fetch_active_addresses(chain: str = "ethereum") -> Dict[str, Any]:
184
+ cache_key = f"active_addresses_{chain}"
185
+ if cache_key in CACHE and time.time() - CACHE_TIMES.get(cache_key, 0) < 600:
186
+ return CACHE[cache_key]
187
+
188
+ try:
189
+ r = requests.get(
190
+ f"https://api.llama.fi/overview/{chain}?excludeTotalDataChart=true&excludeTotalDataChartBreakdown=true",
191
+ timeout=10
192
+ )
193
+ if r.status_code == 200:
194
+ data = r.json()
195
+ active_users = data.get('activeUsers', 0)
196
+
197
+ prev = CACHE.get(f"{cache_key}_prev", active_users)
198
+ change = ((active_users - prev) / prev * 100) if prev > 0 else 0
199
+ CACHE[f"{cache_key}_prev"] = active_users
200
 
201
+ is_anomaly = abs(change) > 20 # Резкий скачок >20% = аномалия
202
+
203
+ result = {
204
+ 'chain': chain,
205
+ 'active_users': active_users,
206
+ 'change_pct': round(change, 2),
207
+ 'is_anomaly': is_anomaly,
208
+ 'signal': 'SPIKE_UP' if change > 20 else 'SPIKE_DOWN' if change < -20 else 'NORMAL'
209
+ }
210
  CACHE[cache_key] = result
211
  CACHE_TIMES[cache_key] = time.time()
212
  return result
213
  except:
214
  pass
215
 
216
+ return {'active_users': 0, 'is_anomaly': False, 'signal': 'NORMAL'}
217
+
218
+ # ================= ОБЩИЙ ДЕТЕКТОР АНОМАЛИЙ =================
219
+ def detect_anomalies(symbol: str) -> Dict[str, Any]:
220
+ chain = "ethereum" if "ETH" in symbol else "solana"
221
+ native = "ETH" if "ETH" in symbol else "SOL"
222
+
223
+ gas = fetch_gas_oracle() if chain == "ethereum" else {'is_anomaly': False}
224
+ large_tx = fetch_large_transactions() if chain == "ethereum" else {'is_anomaly': False, 'signal': 'NORMAL'}
225
+ flows = fetch_exchange_flows(native)
226
+ active = fetch_active_addresses(chain)
227
+
228
+ anomalies = []
229
+ anomaly_score = 0
230
+
231
+ if gas.get('is_anomaly'):
232
+ anomalies.append({"type": "GAS_SPIKE", "severity": "HIGH", "reason": f"Газ: {gas.get('avg_gas')} gwei"})
233
+ anomaly_score += 30
234
+
235
+ if large_tx.get('is_anomaly'):
236
+ anomalies.append({"type": "WHALE_MOVEMENT", "severity": "HIGH", "reason": f"Китовых TX: {large_tx.get('count')}"})
237
+ anomaly_score += 25
238
+
239
+ if flows.get('is_anomaly'):
240
+ direction = flows.get('signal', '')
241
+ if direction == 'OUTFLOW':
242
+ anomalies.append({"type": "EXCHANGE_OUTFLOW", "severity": "HIGH", "reason": "Крупный отток с бирж"})
243
+ anomaly_score += 20
244
+ elif direction == 'INFLOW':
245
+ anomalies.append({"type": "EXCHANGE_INFLOW", "severity": "MEDIUM", "reason": "Крупный приток на биржи"})
246
+ anomaly_score += 15
247
+
248
+ if active.get('is_anomaly'):
249
+ anomalies.append({"type": "ADDRESS_SPIKE", "severity": "MEDIUM", "reason": f"Адреса: {active.get('signal')} ({active.get('change_pct')}%)"})
250
+ anomaly_score += 15
251
+
252
+ anomaly_score = min(100, anomaly_score)
253
+
254
+ if anomaly_score >= 60:
255
+ level = "CRITICAL"
256
+ signal = "WAIT" # При критических аномалиях — не торгуем
257
+ elif anomaly_score >= 30:
258
+ level = "ELEVATED"
259
+ signal = "CAUTION"
260
  else:
261
+ level = "NORMAL"
262
  signal = "NEUTRAL"
263
 
264
+ if anomalies:
265
+ ANOMALY_HISTORY.append({
266
+ "timestamp": datetime.utcnow().isoformat(),
267
+ "symbol": symbol,
268
+ "anomalies": anomalies,
269
+ "score": anomaly_score
270
+ })
271
+
272
  return {
273
+ "anomaly_score": anomaly_score,
274
+ "anomaly_level": level,
275
+ "signal": signal,
276
+ "anomalies": anomalies,
277
+ "metrics": {
278
+ "gas": gas,
279
+ "large_transactions": large_tx,
280
+ "exchange_flows": flows,
281
+ "active_addresses": active
282
+ }
283
  }
284
 
285
  # ================= АНАЛИЗ ИЗ MT5 =================
286
  def analyze_from_mt5(mt5_features: Dict[str, Any]) -> Optional[Dict[str, Any]]:
287
  try:
288
+ anomalies = []
289
+ score = 0
290
+
291
+ atr_pct = mt5_features.get('H1_atr_pct', 1)
292
+ if isinstance(atr_pct, (int, float)) and atr_pct > 3.0:
293
+ anomalies.append({"type": "VOLATILITY_SPIKE", "severity": "HIGH", "reason": f"ATR {atr_pct}%"})
294
+ score += 25
295
+
296
+ volume = mt5_features.get('H1_volume', 0)
297
+ if isinstance(volume, (int, float)) and volume > 5000:
298
+ anomalies.append({"type": "VOLUME_SPIKE", "severity": "MEDIUM", "reason": f"Объём {volume}"})
299
+ score += 15
300
+
301
+ if score >= 30:
302
+ signal = "WAIT"
303
+ elif score >= 15:
304
+ signal = "CAUTION"
 
 
 
 
 
 
 
 
 
 
 
305
  else:
306
+ signal = "NEUTRAL"
 
307
 
308
  return {
309
+ "anomaly_score": score,
310
+ "signal": signal,
311
+ "anomalies": anomalies,
 
312
  "source": "MT5"
313
  }
314
  except:
315
  return None
316
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
  # ================= ГЛАВНЫЙ СИГНАЛ =================
318
+ def get_anomaly_signal(symbol: str = "ETH/USD") -> Dict[str, Any]:
319
  start = time.time()
320
 
321
  # Проверка MT5
322
+ if symbol in FEATURES_STORE:
323
+ fs = FEATURES_STORE[symbol]
324
+ age = time.time() - fs.get("timestamp", 0)
325
+ if age < MT5_MAX_AGE_SEC:
326
+ mt5_features = fs.get("features", {})
327
+ if mt5_features:
328
+ mt5_result = analyze_from_mt5(mt5_features)
329
+ if mt5_result and mt5_result["anomaly_score"] > 0:
330
+ result = {
331
+ "space": "space_23_anomaly",
332
+ "timestamp": int(time.time()),
333
+ "symbol": symbol,
334
+ "signal": {"direction": mt5_result["signal"], "confidence": min(0.9, mt5_result["anomaly_score"] / 50)},
335
+ "anomaly_analysis": {
336
+ "score": mt5_result["anomaly_score"],
337
+ "anomalies": mt5_result["anomalies"]
338
+ },
339
+ "data_source": "MT5",
340
+ "meta": {"latency_ms": int((time.time() - start) * 1000)}
341
+ }
342
+ send_to_arbiter(result)
343
+ print(f"🔍 ANOMALY {symbol}: Score={mt5_result['anomaly_score']} (MT5)")
344
+ return result
345
+
346
+ # Fallback
347
+ analysis = detect_anomalies(symbol)
 
 
 
 
 
 
 
 
348
  latency = int((time.time() - start) * 1000)
349
 
350
+ direction = "WAIT" if analysis['anomaly_level'] in ['CRITICAL', 'ELEVATED'] else "NEUTRAL"
351
+ confidence = analysis['anomaly_score'] / 100 if analysis['anomaly_score'] > 0 else 0.0
 
 
352
 
353
  result = {
354
+ "space": "space_23_anomaly",
355
  "timestamp": int(time.time()),
356
+ "symbol": symbol,
357
+ "signal": {
358
+ "direction": direction,
359
+ "confidence": round(confidence, 4)
360
+ },
361
+ "anomaly_analysis": {
362
+ "score": analysis['anomaly_score'],
363
+ "level": analysis['anomaly_level'],
364
+ "anomalies": analysis['anomalies'],
365
+ "metrics": analysis['metrics']
366
  },
367
+ "data_source": "ONCHAIN",
368
  "meta": {"latency_ms": latency}
369
  }
370
 
371
  send_to_arbiter(result)
372
+ if analysis['anomalies']:
373
+ print(f"🔍 ANOMALY {symbol}: {analysis['anomaly_level']} | Score={analysis['anomaly_score']} | {len(analysis['anomalies'])} аномалий")
374
  return result
375
 
376
  # ================= KEEP-ALIVE =================
 
385
  threading.Thread(target=keep_alive, daemon=True).start()
386
 
387
  # ================= FASTAPI =================
388
+ app = FastAPI(title="TOMIRIS SPACE 23 v1.0 — ON-CHAIN ANOMALY DETECTOR")
389
 
390
  @app.get("/health")
391
  @app.head("/health")
392
  async def health():
393
  return {
394
+ "space": "Space 23 - On-Chain Anomaly Detector v1.0",
395
  "status": "operational",
396
+ "symbols": TRACKED_SYMBOLS,
397
+ "features": ["Gas Spike", "Whale TX", "Exchange Flows", "Active Addresses"],
398
+ "anomaly_history": len(ANOMALY_HISTORY)
399
  }
400
 
401
  @app.get("/consilium")
402
+ async def consilium(symbol: str = Query("ETH/USD")):
403
+ if symbol not in TRACKED_SYMBOLS:
404
+ return {"error": f"Unsupported: {symbol}"}
405
+ return get_anomaly_signal(symbol)
406
 
407
+ @app.get("/scan/{symbol}")
408
+ async def scan(symbol: str):
409
+ if symbol not in TRACKED_SYMBOLS:
410
  return {"error": f"Unsupported: {symbol}"}
411
+ return detect_anomalies(symbol)
412
 
413
+ @app.get("/gas")
414
+ async def gas():
415
+ return fetch_gas_oracle()
416
 
417
+ @app.get("/whales")
418
+ async def whales():
419
+ return fetch_large_transactions()
 
 
420
 
421
+ @app.get("/flows/{symbol}")
422
+ async def flows(symbol: str = "ETH"):
423
+ return fetch_exchange_flows(symbol)
424
+
425
+ @app.get("/active/{chain}")
426
+ async def active(chain: str = "ethereum"):
427
+ return fetch_active_addresses(chain)
428
+
429
+ @app.get("/history")
430
+ async def history(limit: int = Query(20)):
431
+ return list(ANOMALY_HISTORY)[-limit:]
432
 
433
  @app.post("/features")
434
  async def receive_features(data: Dict[str, Any]):
435
+ symbol = data.get("symbol", "ETH/USD")
436
  FEATURES_STORE[symbol] = {
437
  "features": data.get("features", {}),
438
  "price": data.get("price", 0.0),
 
441
  print(f"📥 MT5 {symbol}: {len(data.get('features', {}))} признаков")
442
  return {"status": "ok"}
443
 
444
+ print(f"🚀 SPACE 23 v1.0 — ON-CHAIN ANOMALY DETECTOR ЗАПУЩЕН!")
445
+ print(f"🔍 Детектор: Gas Spike | Whale TX | Exchange Flows | Active Addresses")
446
  print(f"✅ Готов к бою!")