Spaces:
Paused
Paused
| # ============================================ | |
| # АВТО-УСТАНОВКА ПАКЕТОВ | |
| # ============================================ | |
| import subprocess, sys, importlib | |
| REQUIRED_PACKAGES = { | |
| 'numpy': 'numpy', | |
| 'httpx': 'httpx', | |
| 'fastapi': 'fastapi', | |
| 'uvicorn': 'uvicorn', | |
| 'requests': 'requests' | |
| } | |
| for module_name, pip_name in REQUIRED_PACKAGES.items(): | |
| try: | |
| importlib.import_module(module_name) | |
| except ImportError: | |
| print(f"📦 Устанавливаю {pip_name}...") | |
| subprocess.check_call([sys.executable, "-m", "pip", "install", pip_name]) | |
| print(f"✅ {pip_name} установлен!") | |
| # ============================================ | |
| # 👑 TOMIRIS SPACE 22 v2.2 — SOCIAL SENTIMENT ENGINE (АВТО-ОТПРАВКА) | |
| # ============================================ | |
| import os, time, json, logging, asyncio, xml.etree.ElementTree as ET | |
| from typing import Dict, Any, List, Optional | |
| from datetime import datetime, timezone | |
| from collections import deque | |
| import numpy as np | |
| import httpx | |
| from fastapi import FastAPI, Query | |
| logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") | |
| logger = logging.getLogger("Space22_SocialSentiment") | |
| # ================= КОНФИГУРАЦИЯ ================= | |
| SYMBOLS = ["XAU/USD", "ETH/USD", "SOL/USD"] | |
| HUB_URL = os.getenv("SPACE17_URL", "https://tomiris-ai-name5-5.hf.space") | |
| NEWSAPI_KEY = os.getenv("NEWSAPI_KEY", "") | |
| # Интервал авто-отправки | |
| AUTO_SEND_INTERVAL = int(os.getenv("AUTO_SEND_INTERVAL", "600")) # 10 мин (Reddit API щадящий) | |
| FEAR_KW = { | |
| "XAU/USD": ["gold crash", "gold selloff", "gold bubble", "gold plummet"], | |
| "ETH/USD": ["ethereum crash", "eth sell", "defi hack", "eth dump", "eth bear"], | |
| "SOL/USD": ["solana crash", "sol dump", "solana outage", "sol hack", "sol bear"] | |
| } | |
| GREED_KW = { | |
| "XAU/USD": ["gold moon", "gold rally", "buy gold", "gold safe haven"], | |
| "ETH/USD": ["ethereum moon", "eth pump", "eth breakout", "buy eth"], | |
| "SOL/USD": ["solana moon", "sol pump", "sol breakout", "buy sol"] | |
| } | |
| NEGATION_WORDS = ["not", "don't", "no", "never", "isn't", "won't"] | |
| MEME_FILE = "meme_history.json" | |
| # ================= ЗАГРУЗКА МЕМОВ ================= | |
| if os.path.exists(MEME_FILE): | |
| try: | |
| with open(MEME_FILE) as f: | |
| MEME_HISTORY = deque(json.load(f), maxlen=200) | |
| except: | |
| MEME_HISTORY = deque(maxlen=200) | |
| else: | |
| MEME_HISTORY = deque(maxlen=200) | |
| def save_meme_history(): | |
| with open(MEME_FILE, 'w') as f: | |
| json.dump(list(MEME_HISTORY), f) | |
| # ================= HTTP КЛИЕНТ ================= | |
| http_client = httpx.AsyncClient(timeout=15.0, headers={"User-Agent": "Tomiris-Space22-v2.2"}) | |
| # ================= ИНСТРУМЕНТЫ АНАЛИЗА ================= | |
| def simple_sentiment(text: str, fear_words: List[str], greed_words: List[str]) -> float: | |
| text_lower = text.lower() | |
| fear_score = 0 | |
| greed_score = 0 | |
| for kw in fear_words: | |
| if kw in text_lower: | |
| words = text_lower.split() | |
| try: | |
| idx = words.index(kw.split()[-1]) | |
| preceding = ' '.join(words[max(0, idx-2):idx]) | |
| if any(neg in preceding for neg in NEGATION_WORDS): | |
| greed_score += 0.5 | |
| else: | |
| fear_score += 1 | |
| except: | |
| fear_score += 1 | |
| for kw in greed_words: | |
| if kw in text_lower: | |
| words = text_lower.split() | |
| try: | |
| idx = words.index(kw.split()[-1]) | |
| preceding = ' '.join(words[max(0, idx-2):idx]) | |
| if any(neg in preceding for neg in NEGATION_WORDS): | |
| fear_score += 0.5 | |
| else: | |
| greed_score += 1 | |
| except: | |
| greed_score += 1 | |
| total = fear_score + greed_score | |
| if total == 0: | |
| return 0.0 | |
| return (greed_score - fear_score) / total | |
| # ================= СБОР ДАННЫХ ================= | |
| async def fetch_reddit_sentiment(symbol: str) -> Dict[str, Any]: | |
| try: | |
| query = symbol.replace("/USD", "").lower() | |
| if "xau" in query: | |
| query = "gold" | |
| headers = {'User-Agent': 'Mozilla/5.0'} | |
| url = f"https://www.reddit.com/r/CryptoCurrency+wallstreetbets+investing/search.json?q={query}&sort=new&limit=25" | |
| r = await http_client.get(url, headers=headers) | |
| if r.status_code == 200: | |
| posts = r.json().get('data', {}).get('children', []) | |
| sentiments = [] | |
| total_weight = 0.0 | |
| for post in posts: | |
| data = post.get('data', {}) | |
| title = data.get('title', '') | |
| text = data.get('selftext', '')[:200] | |
| full = title + ' ' + text | |
| ups = data.get('ups', 0) | |
| comments = data.get('num_comments', 0) | |
| weight = np.log1p(ups + comments * 2) | |
| score = simple_sentiment(full, FEAR_KW.get(symbol, []), GREED_KW.get(symbol, [])) | |
| sentiments.append(score * weight) | |
| total_weight += weight | |
| if total_weight > 0: | |
| avg_sentiment = sum(sentiments) / total_weight | |
| else: | |
| avg_sentiment = 0.0 | |
| if avg_sentiment > 0.3: | |
| sentiment, signal = "GREED", "BEARISH" | |
| elif avg_sentiment < -0.3: | |
| sentiment, signal = "FEAR", "BULLISH" | |
| else: | |
| sentiment, signal = "NEUTRAL", "NEUTRAL" | |
| return { | |
| 'sentiment': sentiment, | |
| 'score': round(avg_sentiment, 4), | |
| 'market_signal': signal, | |
| 'posts_analyzed': len(posts), | |
| 'source': 'Reddit' | |
| } | |
| except Exception as e: | |
| logger.warning(f"Reddit {symbol}: {e}") | |
| return {'sentiment': 'NEUTRAL', 'score': 0.0, 'market_signal': 'NEUTRAL', 'source': 'Reddit'} | |
| async def fetch_news_attention(symbol: str) -> Dict[str, Any]: | |
| try: | |
| query = symbol.replace("/USD", "").lower() | |
| if "xau" in query: | |
| query = "gold price" | |
| url = f"https://news.google.com/rss/search?q={query}&hl=en-US&ceid=US:en" | |
| r = await http_client.get(url) | |
| if r.status_code == 200: | |
| root = ET.fromstring(r.content) | |
| items = root.findall("./channel/item") | |
| count = len(items) | |
| if count > 30: | |
| level = "EXTREME" | |
| signal = "BEARISH" if "crypto" in query else "BULLISH" | |
| elif count > 20: | |
| level = "HIGH" | |
| signal = "BEARISH" if "crypto" in query else "BULLISH" | |
| elif count > 10: | |
| level = "MODERATE" | |
| signal = "NEUTRAL" | |
| else: | |
| level = "LOW" | |
| signal = "BULLISH" if "crypto" in query else "NEUTRAL" | |
| return { | |
| 'mention_count': count, | |
| 'level': level, | |
| 'market_signal': signal, | |
| 'source': 'Google News RSS' | |
| } | |
| except: | |
| pass | |
| return {'mention_count': 0, 'level': 'LOW', 'market_signal': 'NEUTRAL', 'source': 'Google News RSS'} | |
| def update_meme_history(symbol: str): | |
| MEME_HISTORY.append({ | |
| "timestamp": time.time(), | |
| "symbol": symbol, | |
| "type": "social_scan" | |
| }) | |
| save_meme_history() | |
| def analyze_meme_activity() -> Dict[str, Any]: | |
| recent = [m for m in MEME_HISTORY if time.time() - m.get('timestamp', 0) < 3600] | |
| count = len(recent) | |
| if count > 30: | |
| level, signal = "EXTREME", "BEARISH" | |
| elif count > 15: | |
| level, signal = "HIGH", "SLIGHTLY_BEARISH" | |
| elif count > 5: | |
| level, signal = "MODERATE", "NEUTRAL" | |
| else: | |
| level, signal = "LOW", "NEUTRAL" | |
| return {'meme_level': level, 'recent_memes': count, 'market_signal': signal, 'source': 'Meme Detector'} | |
| # ================= АНАЛИЗ ================= | |
| async def analyze_sentiment(symbol: str) -> Dict[str, Any]: | |
| reddit, news = await asyncio.gather( | |
| fetch_reddit_sentiment(symbol), | |
| fetch_news_attention(symbol) | |
| ) | |
| meme = analyze_meme_activity() | |
| signals = [] | |
| score = 50.0 | |
| if reddit['market_signal'] == 'BULLISH': | |
| signals.append({"source": "Reddit", "signal": "BULLISH", "reason": f"Страх ({reddit['score']:.2f})"}) | |
| score += 20 | |
| elif reddit['market_signal'] == 'BEARISH': | |
| signals.append({"source": "Reddit", "signal": "BEARISH", "reason": f"Жадность ({reddit['score']:.2f})"}) | |
| score -= 20 | |
| if news['market_signal'] == 'BULLISH': | |
| signals.append({"source": "News", "signal": "BULLISH", "reason": f"Внимание СМИ: {news['level']}"}) | |
| score += 10 | |
| elif news['market_signal'] == 'BEARISH': | |
| signals.append({"source": "News", "signal": "BEARISH", "reason": f"СМИ хайп: {news['level']}"}) | |
| score -= 10 | |
| if meme['market_signal'] == 'BEARISH': | |
| signals.append({"source": "Meme", "signal": "BEARISH", "reason": f"Мемов: {meme['meme_level']}"}) | |
| score -= 10 | |
| elif meme['market_signal'] == 'SLIGHTLY_BEARISH': | |
| signals.append({"source": "Meme", "signal": "CAUTION", "reason": "Много мемов"}) | |
| score -= 5 | |
| update_meme_history(symbol) | |
| score = max(0, min(100, score)) | |
| if score > 60: | |
| direction, confidence = "LONG", score / 100 | |
| elif score < 40: | |
| direction, confidence = "SHORT", (100 - score) / 100 | |
| else: | |
| direction, confidence = "WAIT", 0.0 | |
| return { | |
| "sentiment_score": score, | |
| "direction": direction, | |
| "confidence": round(confidence, 4), | |
| "signals": signals, | |
| "metrics": { | |
| "reddit": reddit, | |
| "news_attention": news, | |
| "meme_activity": meme | |
| } | |
| } | |
| # ================= ОТПРАВКА В HUB ================= | |
| async def send_signal_to_hub(symbol: str, direction: str, confidence: float): | |
| try: | |
| resp = await http_client.post(f"{HUB_URL}/signal", json={ | |
| "space": "space_22_sentiment", | |
| "symbol": symbol, | |
| "direction": direction, | |
| "confidence": confidence, | |
| "raw": json.dumps({"source": "space_22_sentiment"}) | |
| }, timeout=10) | |
| if resp.status_code == 200: | |
| logger.info(f"📤 {symbol}: {direction} conf={confidence:.3f} отправлен в Hub") | |
| else: | |
| logger.warning(f"Hub вернул {resp.status_code}") | |
| except Exception as e: | |
| logger.error(f"Ошибка отправки в Hub: {e}") | |
| # ================= ГЛАВНЫЙ СИГНАЛ ================= | |
| async def get_sentiment_signal() -> Dict[str, Any]: | |
| start = time.time() | |
| tasks = [analyze_sentiment(sym) for sym in SYMBOLS] | |
| results = await asyncio.gather(*tasks) | |
| signals_dict = {} | |
| for sym, res in zip(SYMBOLS, results): | |
| signals_dict[sym] = { | |
| "direction": res['direction'], | |
| "confidence": res['confidence'], | |
| "sentiment_score": res['sentiment_score'] | |
| } | |
| latency = int((time.time() - start) * 1000) | |
| for sym in SYMBOLS: | |
| await send_signal_to_hub(sym, signals_dict[sym]['direction'], signals_dict[sym]['confidence']) | |
| result = { | |
| "space": "space_22_sentiment", | |
| "timestamp": int(time.time()), | |
| "signals": signals_dict, | |
| "sentiment_analysis": { | |
| "gold": results[0], | |
| "eth": results[1], | |
| "sol": results[2] | |
| }, | |
| "latency_ms": latency | |
| } | |
| logger.info(f"💬 Sentiment: XAU={signals_dict['XAU/USD']['direction']} " | |
| f"ETH={signals_dict['ETH/USD']['direction']} SOL={signals_dict['SOL/USD']['direction']}") | |
| return result | |
| # ================= АВТО-ОТПРАВКА ================= | |
| async def auto_send_loop(): | |
| logger.info(f"🔄 Авто-отправка Sentiment запущена (интервал {AUTO_SEND_INTERVAL}с)") | |
| await asyncio.sleep(30) | |
| while True: | |
| try: | |
| await get_sentiment_signal() | |
| logger.info("✅ Sentiment авто-отправка завершена") | |
| except Exception as e: | |
| logger.error(f"Ошибка авто-отправки: {e}") | |
| await asyncio.sleep(AUTO_SEND_INTERVAL) | |
| # ================= FASTAPI ================= | |
| app = FastAPI(title="Tomiris Space 22 v2.2 — Social Sentiment Engine (Auto-Hub)") | |
| async def startup(): | |
| asyncio.create_task(auto_send_loop()) | |
| logger.info("🚀 Space 22 v2.2 запущен с авто-отправкой в Hub") | |
| async def shutdown(): | |
| await http_client.aclose() | |
| async def health(): | |
| return { | |
| "status": "operational", | |
| "version": "2.2", | |
| "hub_url": HUB_URL, | |
| "auto_send_interval": AUTO_SEND_INTERVAL, | |
| "async": True | |
| } | |
| async def consilium(): | |
| return await get_sentiment_signal() | |
| async def sentiment(symbol: str): | |
| if symbol not in SYMBOLS: | |
| return {"error": "Invalid symbol"} | |
| return await analyze_sentiment(symbol) | |
| async def meme(): | |
| return analyze_meme_activity() | |
| async def reddit(symbol: str): | |
| if symbol not in SYMBOLS: | |
| return {"error": "Invalid symbol"} | |
| return await fetch_reddit_sentiment(symbol) | |
| async def trends(symbol: str): | |
| if symbol not in SYMBOLS: | |
| return {"error": "Invalid symbol"} | |
| return await fetch_news_attention(symbol) | |
| async def send_now(): | |
| return await get_sentiment_signal() | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run(app, host="0.0.0.0", port=7860) | |
| print("🚀 SPACE 22 v2.2 — SOCIAL SENTIMENT ENGINE (АВТО-ОТПРАВКА) ЗАПУЩЕН!") |