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 30 v3.1 — META-ENSEMBLE AI (АВТО-ОТПРАВКА В HUB) | |
| # ============================================ | |
| import os, time, json, logging, asyncio | |
| 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, HTTPException | |
| logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") | |
| logger = logging.getLogger("Space30_MetaEnsemble") | |
| # ================= КОНФИГУРАЦИЯ ================= | |
| SYMBOLS = ["XAU/USD", "ETH/USD", "SOL/USD"] | |
| SPACE31_URL = os.getenv("SPACE31_URL", "").rstrip("/") # Performance Engine | |
| SPACE17_URL = os.getenv("SPACE17_URL", "https://tomiris-ai-name5-5.hf.space").rstrip("/") | |
| # Интервал авто-отправки (секунды) | |
| AUTO_SEND_INTERVAL = int(os.getenv("AUTO_SEND_INTERVAL", "300")) # 5 минут | |
| # Группы аналитиков (без Space 18 – рекурсия исключена) | |
| ANALYST_GROUPS = { | |
| "technical": { | |
| "space_1_xau_master": "https://nuxotetotmailsvoboden-tomiris.hf.space", | |
| "space_2_eth_master": "https://nuxotetotmailsvoboden-tomiris-falcon-ai.hf.space", | |
| "space_3_mtf": "https://nuxotetotmailsvoboden-tomiris-smollm-ai.hf.space", | |
| "space_4_patterns": "https://nuxotetotmailsvoboden-tomiris-llama-ai.hf.space", | |
| "space_6_arbitrage": "https://nuxotetotmailsvoboden-tomiris-agents.hf.space", | |
| "space_16_quant": "https://tomiris-ai-name4-4.hf.space", | |
| "space_19_sol": "https://tomirisai80-tomirisanal.hf.space", | |
| "space_25_correlation": "https://tomirisg25-tomirisgold1.hf.space", | |
| }, | |
| "macro": { | |
| "space_7_fred": "https://nuxotetotnicksvoboden-name-1.hf.space", | |
| "space_11_behavioral": "https://nuxotetotnicksvoboden-name5.hf.space", | |
| "space_12_macro_agg": "https://nuxotetotnicksvoboden-name6.hf.space", | |
| "space_21_gold_macro": "https://tomirisai80-tomirisanal3.hf.space", | |
| "space_24_eco_agg": "https://tomirisai80-tomirisanal6.hf.space", | |
| "space_27_seasonality": "https://tomirisg25-tomirisgold6-1.hf.space", | |
| "space_29_macro_surprise": "https://tomirisg25-tomirisgold5.hf.space", | |
| }, | |
| "onchain": { | |
| "space_9_onchain": "https://nuxotetotnicksvoboden-name3.hf.space", | |
| "space_20_l2_defi": "https://tomirisai80-tomirisanal2.hf.space", | |
| }, | |
| "risk": { | |
| "space_5_risk": "https://nuxotetotmailsvoboden-tomiris-mistral.hf.space", | |
| "space_14_portfolio": "https://tomiris-ai-name2-2.hf.space", | |
| "space_23_anomaly": "https://tomirisai80-tomirisai5.hf.space", | |
| "space_26_options": "https://tomirisg25-tomirisgold2.hf.space", | |
| "space_28_regime": "https://tomirisg25-tomirisgold4.hf.space", | |
| }, | |
| "sentiment": { | |
| "space_8_news": "https://nuxotetotnicksvoboden-name2.hf.space", | |
| "space_10_whales": "https://nuxotetotnicksvoboden-name4.hf.space", | |
| "space_22_sentiment": "https://tomirisai80-tomirisanal4.hf.space", | |
| }, | |
| "ai": { | |
| "space_13_qwen": "https://tomiris-ai-name1-1.hf.space", | |
| }, | |
| "analysis": { | |
| "space_15_backtest": "https://tomiris-ai-name3-3.hf.space", | |
| }, | |
| } | |
| # ================= HTTP КЛИЕНТ ================= | |
| http_client = httpx.AsyncClient(timeout=12.0) | |
| # ================= ГЛОБАЛЬНЫЙ КЭШ ================= | |
| cache_store = {} | |
| cache_times = {} | |
| # История решений | |
| HISTORY_FILE = "meta_ensemble_history.json" | |
| if os.path.exists(HISTORY_FILE): | |
| try: | |
| with open(HISTORY_FILE) as f: | |
| DECISION_HISTORY = deque(json.load(f), maxlen=500) | |
| except: | |
| DECISION_HISTORY = deque(maxlen=500) | |
| else: | |
| DECISION_HISTORY = deque(maxlen=500) | |
| def save_history(): | |
| with open(HISTORY_FILE, 'w') as f: | |
| json.dump(list(DECISION_HISTORY), f) | |
| # ================= ПОЛУЧЕНИЕ ГОЛОСА ================= | |
| async def fetch_vote(name: str, url: str, symbol: str) -> Dict: | |
| cache_key = f"{name}_{symbol}" | |
| now = time.time() | |
| if cache_key in cache_store and (now - cache_times.get(cache_key, 0)) < 30: | |
| return cache_store[cache_key] | |
| try: | |
| r = await http_client.get(f"{url}/consilium?symbol={symbol}") | |
| if r.status_code == 200: | |
| data = r.json() | |
| signal = data.get("signal", {}) | |
| direction = signal.get("direction", "WAIT") | |
| confidence = signal.get("confidence", 0.0) | |
| result = { | |
| "direction": direction, | |
| "confidence": confidence, | |
| "active": True | |
| } | |
| cache_store[cache_key] = result | |
| cache_times[cache_key] = now | |
| return result | |
| except: | |
| pass | |
| return { | |
| "direction": "WAIT", | |
| "confidence": 0.0, | |
| "active": False | |
| } | |
| # ================= ДИНАМИЧЕСКИЕ ВЕСА ================= | |
| async def get_dynamic_weights() -> Dict[str, float]: | |
| # Пробуем Space 31 (Performance Engine) | |
| if SPACE31_URL: | |
| try: | |
| r = await http_client.get(f"{SPACE31_URL}/weights") | |
| if r.status_code == 200: | |
| data = r.json() | |
| return {k: v["weight"] for k, v in data.items()} | |
| except: | |
| pass | |
| # Fallback на метрики Space 17 (Data Hub) | |
| try: | |
| r = await http_client.get(f"{SPACE17_URL}/metrics") | |
| if r.status_code == 200: | |
| metrics = r.json() | |
| weights = {} | |
| for m in metrics: | |
| name = m.get("space_name", "") | |
| acc = m.get("accuracy", 0.5) | |
| pf = m.get("profit_factor", 1.0) | |
| weights[name] = acc * min(pf, 3.0) / 3.0 | |
| return weights | |
| except: | |
| pass | |
| # Равные веса — fallback | |
| all_names = [name for group in ANALYST_GROUPS.values() for name in group] | |
| return {name: 1.0 for name in all_names} | |
| # ================= АГРЕГАЦИЯ С ГРУППОВЫМ ГОЛОСОВАНИЕМ ================= | |
| async def aggregate_meta_ensemble(symbol: str) -> Dict: | |
| dyn_weights = await get_dynamic_weights() | |
| # Собираем все голоса параллельно | |
| all_names = [] | |
| tasks = [] | |
| for group, members in ANALYST_GROUPS.items(): | |
| for name, url in members.items(): | |
| all_names.append(name) | |
| tasks.append(fetch_vote(name, url, symbol)) | |
| results = await asyncio.gather(*tasks) | |
| votes = dict(zip(all_names, results)) | |
| # Групповые голоса | |
| group_votes = {} | |
| for group, members in ANALYST_GROUPS.items(): | |
| long_score = 0.0 | |
| short_score = 0.0 | |
| wait_score = 0.0 | |
| total_weight = 0.0 | |
| active_count = 0 | |
| for name in members: | |
| vote = votes.get(name, {"active": False}) | |
| if not vote["active"]: | |
| continue | |
| w = dyn_weights.get(name, 0.5) | |
| conf = vote["confidence"] | |
| if vote["direction"] == "LONG": | |
| long_score += w * conf | |
| elif vote["direction"] == "SHORT": | |
| short_score += w * conf | |
| else: | |
| wait_score += w * conf | |
| total_weight += w | |
| active_count += 1 | |
| if total_weight > 0: | |
| group_votes[group] = { | |
| "LONG": round(long_score / total_weight, 4), | |
| "SHORT": round(short_score / total_weight, 4), | |
| "WAIT": round(wait_score / total_weight, 4), | |
| "active": active_count | |
| } | |
| else: | |
| group_votes[group] = { | |
| "LONG": 0.0, | |
| "SHORT": 0.0, | |
| "WAIT": 0.0, | |
| "active": 0 | |
| } | |
| # Общее голосование с учётом группового консенсуса | |
| long_total = sum(gv["LONG"] for gv in group_votes.values()) | |
| short_total = sum(gv["SHORT"] for gv in group_votes.values()) | |
| wait_total = sum(gv["WAIT"] for gv in group_votes.values()) | |
| total_weight = long_total + short_total + wait_total | |
| if total_weight == 0: | |
| return { | |
| "direction": "WAIT", | |
| "confidence": 0.0, | |
| "active_spaces": 0, | |
| "reason": "Нет голосов" | |
| } | |
| long_pct = long_total / total_weight | |
| short_pct = short_total / total_weight | |
| wait_pct = wait_total / total_weight | |
| # WAIT доминирует → WAIT | |
| if wait_pct > 0.6: | |
| direction = "WAIT" | |
| confidence = wait_pct | |
| elif long_pct > short_pct * 1.2 and long_pct > 0.15: | |
| direction = "LONG" | |
| confidence = min(0.9, long_pct) | |
| elif short_pct > long_pct * 1.2 and short_pct > 0.15: | |
| direction = "SHORT" | |
| confidence = min(0.9, short_pct) | |
| else: | |
| direction = "WAIT" | |
| confidence = max(long_pct, short_pct) | |
| # Консенсус и конфликт | |
| consensus_strength = "STRONG" if confidence > 0.7 else "MODERATE" if confidence > 0.4 else "WEAK" | |
| conflict_detected = (long_pct > 0.2 and short_pct > 0.2) | |
| # Health Score | |
| alive = sum(1 for v in votes.values() if v["active"]) | |
| total = len(all_names) | |
| health_score = alive / total if total else 0 | |
| result = { | |
| "direction": direction, | |
| "confidence": round(confidence, 4), | |
| "active_spaces": alive, | |
| "total_spaces": total, | |
| "health_score": round(health_score, 2), | |
| "consensus_strength": consensus_strength, | |
| "conflict_detected": conflict_detected, | |
| "group_votes": group_votes, | |
| "votes_summary": { | |
| "LONG": round(long_pct, 4), | |
| "SHORT": round(short_pct, 4), | |
| "WAIT": round(wait_pct, 4) | |
| }, | |
| } | |
| # Сохраняем в историю | |
| DECISION_HISTORY.append({ | |
| "timestamp": datetime.now(timezone.utc).isoformat(), | |
| "symbol": symbol, | |
| "direction": direction, | |
| "confidence": confidence, | |
| "health": health_score | |
| }) | |
| save_history() | |
| return result | |
| # ================= ОТПРАВКА В HUB ================= | |
| async def send_signal_to_hub(symbol: str, direction: str, confidence: float): | |
| """Отправка агрегированного сигнала в Space 17 (Data Hub).""" | |
| try: | |
| resp = await http_client.post(f"{SPACE17_URL}/signal", json={ | |
| "space": "space_30_meta_ensemble", | |
| "symbol": symbol, | |
| "direction": direction, | |
| "confidence": confidence, | |
| "raw": json.dumps({"source": "space_30_meta_ensemble"}) | |
| }, timeout=10) | |
| if resp.status_code == 200: | |
| logger.info(f"📤 {symbol}: {direction} conf={confidence:.3f} отправлен в Hub") | |
| else: | |
| logger.warning(f"Hub вернул {resp.status_code}: {resp.text[:100]}") | |
| except Exception as e: | |
| logger.error(f"Ошибка отправки в Hub: {e}") | |
| # ================= ГЛАВНЫЙ СИГНАЛ ================= | |
| async def get_meta_ensemble_signal(symbol: str = "XAU/USD") -> Dict: | |
| start = time.time() | |
| meta = await aggregate_meta_ensemble(symbol) | |
| latency = int((time.time() - start) * 1000) | |
| # Отправка агрегированного сигнала в Hub | |
| await send_signal_to_hub(symbol, meta['direction'], meta['confidence']) | |
| result = { | |
| "space": "space_30_meta_ensemble", | |
| "timestamp": int(time.time()), | |
| "symbol": symbol, | |
| "signal": { | |
| "direction": meta["direction"], | |
| "confidence": meta["confidence"] | |
| }, | |
| "meta": meta, | |
| "latency_ms": latency | |
| } | |
| logger.info(f"🧠 Meta-Ensemble {symbol}: {meta['direction']} conf={meta['confidence']:.3f} " | |
| f"health={meta['health_score']:.0%} active={meta['active_spaces']}") | |
| return result | |
| # ================= АВТО-ОТПРАВКА ПО ТАЙМЕРУ ================= | |
| async def auto_send_loop(): | |
| """🔥 Фоновая задача: каждые N секунд собирает голоса и шлёт агрегированный сигнал в Hub.""" | |
| logger.info(f"🔄 Авто-отправка Meta-Ensemble запущена (интервал {AUTO_SEND_INTERVAL}с)") | |
| # Первый запуск через 30 секунд после старта | |
| await asyncio.sleep(30) | |
| while True: | |
| try: | |
| logger.info("🧠 Meta-Ensemble авто-анализ...") | |
| for symbol in SYMBOLS: | |
| await get_meta_ensemble_signal(symbol) | |
| await asyncio.sleep(3) # Пауза между символами — много запросов | |
| logger.info("✅ Meta-Ensemble авто-отправка завершена") | |
| except Exception as e: | |
| logger.error(f"Ошибка в авто-отправке: {e}") | |
| await asyncio.sleep(AUTO_SEND_INTERVAL) | |
| # ================= FASTAPI ================= | |
| app = FastAPI(title="Tomiris Meta-Ensemble v3.1 Ultimate Consensus (Auto-Hub)") | |
| async def startup(): | |
| # Запускаем фоновую авто-отправку | |
| asyncio.create_task(auto_send_loop()) | |
| logger.info("🚀 Space 30 v3.1 запущен с авто-отправкой в Hub") | |
| async def shutdown(): | |
| await http_client.aclose() | |
| async def health(): | |
| return { | |
| "status": "alive", | |
| "version": "3.1", | |
| "hub_url": SPACE17_URL, | |
| "auto_send_interval": AUTO_SEND_INTERVAL, | |
| "space31_connected": bool(SPACE31_URL) | |
| } | |
| async def consilium(symbol: str = Query("XAU/USD")): | |
| if symbol not in SYMBOLS: | |
| raise HTTPException(status_code=400, detail="Invalid symbol") | |
| return await get_meta_ensemble_signal(symbol) | |
| async def send_now(): | |
| """Ручной триггер отправки всех символов.""" | |
| results = {} | |
| for symbol in SYMBOLS: | |
| analysis = await get_meta_ensemble_signal(symbol) | |
| results[symbol] = analysis.get("signal", {}).get("direction", "WAIT") | |
| return {"status": "sent", "results": results} | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run(app, host="0.0.0.0", port=7860) | |
| print("🚀 SPACE 30 v3.1 — META-ENSEMBLE AI (АВТО-ОТПРАВКА В HUB) ЗАПУЩЕН!") |