tomirisai80 commited on
Commit
da06ca2
·
verified ·
1 Parent(s): c4d54f2

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +446 -0
app.py ADDED
@@ -0,0 +1,446 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================
2
+ # АВТО-УСТАНОВКА ПАКЕТОВ
3
+ # ============================================
4
+ import subprocess
5
+ import sys
6
+ import importlib
7
+
8
+ REQUIRED_PACKAGES = {
9
+ 'numpy': 'numpy',
10
+ 'requests': 'requests'
11
+ }
12
+
13
+ for module_name, pip_name in REQUIRED_PACKAGES.items():
14
+ try:
15
+ importlib.import_module(module_name)
16
+ except ImportError:
17
+ print(f"📦 Устанавливаю {pip_name}...")
18
+ subprocess.check_call([sys.executable, "-m", "pip", "install", pip_name])
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
30
+ from typing import Optional, Dict, Any, List
31
+ import time
32
+ import requests
33
+ import threading
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 =================
377
+ def keep_alive():
378
+ while True:
379
+ time.sleep(840)
380
+ try:
381
+ requests.get("http://localhost:7860/health", timeout=5)
382
+ except:
383
+ pass
384
+
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),
439
+ "timestamp": time.time()
440
+ }
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"✅ Готов к бою!")