Spaces:
Paused
Paused
| # ============================================ | |
| # АВТО-УСТАНОВКА ПАКЕТОВ | |
| # ============================================ | |
| import subprocess, sys, importlib | |
| REQUIRED_PACKAGES = { | |
| 'numpy': 'numpy', | |
| 'pandas': 'pandas', | |
| '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 29 v2.2 — MACRO SURPRISE ENGINE (АВТО-ОТПРАВКА В HUB) | |
| # ============================================ | |
| import os, time, json, logging, asyncio | |
| from typing import Dict, Any, List, Optional | |
| from datetime import datetime, timezone, timedelta | |
| from collections import deque | |
| import numpy as np | |
| import pandas as pd | |
| import httpx | |
| from fastapi import FastAPI, Query | |
| logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") | |
| logger = logging.getLogger("Space29_MacroSurprise") | |
| # ================= КОНФИГУРАЦИЯ ================= | |
| SYMBOLS = ["XAU/USD", "ETH/USD", "SOL/USD"] | |
| HUB_URL = os.getenv("SPACE17_URL", "https://tomiris-ai-name5-5.hf.space") | |
| FRED_KEY = os.getenv("FRED_KEY", "faa11c8e2e4beee08c5b966e8b63a513") | |
| NEWSAPI_KEY = os.getenv("NEWSAPI_KEY", "948c7816beea47baa23b054592472d0e") | |
| # Интервал авто-отправки (секунды) | |
| AUTO_SEND_INTERVAL = int(os.getenv("AUTO_SEND_INTERVAL", "3600")) # раз в час — макро-данные медленные | |
| # Консенсус-прогнозы (обновлять ежемесячно) | |
| CONSENSUS = { | |
| "CPI_YOY": 3.2, | |
| "CORE_CPI_YOY": 3.5, | |
| "UNEMPLOYMENT": 4.0, | |
| "NFP": 180000, | |
| "GDP_QOQ": 2.0, | |
| "FED_RATE": 4.25, | |
| "ISM_MANUF": 49.0, | |
| "ISM_SERVICES": 52.0, | |
| "RETAIL_SALES": 0.3, | |
| "DURABLE_GOODS": 0.5 | |
| } | |
| CACHE_TTL = { | |
| "fred": 3600, | |
| "news": 1800 | |
| } | |
| HISTORY_FILE = "surprise_history.json" | |
| # ================= HTTP КЛИЕНТ ================= | |
| http_client = httpx.AsyncClient(timeout=20.0) | |
| # ================= ГЛОБАЛЬНЫЙ КЭШ ================= | |
| cache_store = {} | |
| cache_times = {} | |
| # История сюрпризов | |
| if os.path.exists(HISTORY_FILE): | |
| try: | |
| with open(HISTORY_FILE) as f: | |
| SURPRISE_HISTORY = deque(json.load(f), maxlen=200) | |
| except: | |
| SURPRISE_HISTORY = deque(maxlen=200) | |
| else: | |
| SURPRISE_HISTORY = deque(maxlen=200) | |
| def save_history(): | |
| with open(HISTORY_FILE, 'w') as f: | |
| json.dump(list(SURPRISE_HISTORY), f) | |
| # ================= ЗАГРУЗКА ДАННЫХ FRED ================= | |
| async def fetch_fred_series(series_id: str, months: int = 13) -> List[Dict]: | |
| cache_key = f"fred_{series_id}_{months}" | |
| if cache_key in cache_store and time.time() - cache_times.get(cache_key, 0) < CACHE_TTL["fred"]: | |
| return cache_store[cache_key] | |
| try: | |
| r = await http_client.get( | |
| f"https://api.stlouisfed.org/fred/series/observations?series_id={series_id}&api_key={FRED_KEY}&file_type=json&sort_order=desc&limit={months}" | |
| ) | |
| if r.status_code == 200: | |
| data = r.json() | |
| values = [ | |
| {'date': obs['date'], 'value': float(obs['value'])} | |
| for obs in data.get('observations', []) | |
| if obs['value'] != '.' | |
| ] | |
| cache_store[cache_key] = values | |
| cache_times[cache_key] = time.time() | |
| return values | |
| except Exception as e: | |
| logger.warning(f"FRED {series_id}: {e}") | |
| return [] | |
| def get_yoy_change(data: List[Dict], current_month: str) -> Optional[float]: | |
| current_val = None | |
| prev_val = None | |
| for item in data: | |
| date = item['date'] | |
| if date == current_month: | |
| current_val = item['value'] | |
| year_ago = str(int(date[:4]) - 1) + date[4:] | |
| if date == year_ago and date[:7] == current_month[:7]: | |
| prev_val = item['value'] | |
| if current_val and prev_val: | |
| return ((current_val - prev_val) / prev_val) * 100 | |
| return None | |
| # ================= АНАЛИЗ СЮРПРИЗА ================= | |
| def calc_surprise(actual: float, consensus: float) -> Dict: | |
| if consensus == 0: | |
| return { | |
| "surprise_pct": 0, | |
| "level": "IN_LINE", | |
| "impact": 0, | |
| "direction": "NEUTRAL" | |
| } | |
| surprise_pct = ((actual - consensus) / abs(consensus)) * 100 | |
| if abs(surprise_pct) > 100: | |
| level, impact = "EXTREME_SURPRISE", 30 | |
| elif abs(surprise_pct) > 50: | |
| level, impact = "MAJOR_SURPRISE", 20 | |
| elif abs(surprise_pct) > 20: | |
| level, impact = "MODERATE_SURPRISE", 10 | |
| elif abs(surprise_pct) > 5: | |
| level, impact = "MINOR_SURPRISE", 5 | |
| else: | |
| level, impact = "IN_LINE", 0 | |
| direction = "POSITIVE" if surprise_pct > 0 else "NEGATIVE" if surprise_pct < 0 else "NEUTRAL" | |
| return { | |
| "surprise_pct": round(surprise_pct, 2), | |
| "level": level, | |
| "impact": impact, | |
| "direction": direction | |
| } | |
| # ================= FOMC SURPRISE (через новости) ================= | |
| async def fetch_fomc_surprise() -> Dict: | |
| if not NEWSAPI_KEY: | |
| return { | |
| "indicator": "FOMC", | |
| "impact": 0, | |
| "direction": "NEUTRAL" | |
| } | |
| try: | |
| r = await http_client.get( | |
| f"https://newsapi.org/v2/everything?q=fomc+fed+rate+decision&pageSize=10&apiKey={NEWSAPI_KEY}" | |
| ) | |
| if r.status_code == 200: | |
| articles = r.json().get('articles', []) | |
| hawk = sum( | |
| 1 for a in articles | |
| if any(w in (a.get('title', '') + a.get('description', '')).lower() | |
| for w in ['hawkish', 'raise', 'tighten', 'surprise hike']) | |
| ) | |
| dove = sum( | |
| 1 for a in articles | |
| if any(w in (a.get('title', '') + a.get('description', '')).lower() | |
| for w in ['dovish', 'cut', 'ease', 'surprise cut']) | |
| ) | |
| if hawk > dove * 2: | |
| return { | |
| "indicator": "FOMC", | |
| "impact": -15, | |
| "direction": "HAWKISH", | |
| "hawkish": hawk, | |
| "dovish": dove | |
| } | |
| elif dove > hawk * 2: | |
| return { | |
| "indicator": "FOMC", | |
| "impact": 15, | |
| "direction": "DOVISH", | |
| "hawkish": hawk, | |
| "dovish": dove | |
| } | |
| except: | |
| pass | |
| return { | |
| "indicator": "FOMC", | |
| "impact": 0, | |
| "direction": "NEUTRAL" | |
| } | |
| # ================= ГЛАВНЫЙ АНАЛИЗ ================= | |
| async def analyze_macro_surprises() -> Dict: | |
| today = datetime.now(timezone.utc) | |
| current_month_str = today.strftime("%Y-%m") | |
| # Загружаем все данные параллельно | |
| cpi_data = await fetch_fred_series("CPIAUCSL", 13) | |
| core_cpi_data = await fetch_fred_series("CPILFESL", 13) | |
| unemp_data = await fetch_fred_series("UNRATE", 6) | |
| nfp_data = await fetch_fred_series("PAYEMS", 3) | |
| gdp_data = await fetch_fred_series("GDP", 3) | |
| ism_data = await fetch_fred_series("NAPM", 3) | |
| retail_data = await fetch_fred_series("RSAFS", 3) | |
| durable_data = await fetch_fred_series("DGORDER", 3) | |
| fomc_surprise = await fetch_fomc_surprise() | |
| surprises = [] | |
| total_score = 0.0 | |
| weights = { | |
| "CPI_YOY": 0.30, | |
| "NFP": 0.25, | |
| "FOMC": 0.20, | |
| "ISM_MANUF": 0.10, | |
| "GDP_QOQ": 0.10, | |
| "RETAIL_SALES": 0.05 | |
| } | |
| # CPI YoY | |
| cpi_yoy = get_yoy_change(cpi_data, current_month_str) | |
| if cpi_yoy is not None: | |
| s = calc_surprise(cpi_yoy, CONSENSUS["CPI_YOY"]) | |
| impact = s['impact'] * (1 if s['direction'] == 'NEGATIVE' else -0.5) | |
| total_score += impact * weights["CPI_YOY"] | |
| surprises.append({ | |
| "indicator": "CPI_YOY", | |
| "actual": round(cpi_yoy, 2), | |
| "consensus": CONSENSUS["CPI_YOY"], | |
| "surprise": s | |
| }) | |
| # NFP | |
| if nfp_data: | |
| nfp_actual = nfp_data[0]['value'] | |
| s = calc_surprise(nfp_actual, CONSENSUS["NFP"]) | |
| impact = s['impact'] * (1 if s['direction'] == 'POSITIVE' else -1) | |
| total_score += impact * weights["NFP"] | |
| surprises.append({ | |
| "indicator": "NFP", | |
| "actual": int(nfp_actual), | |
| "consensus": CONSENSUS["NFP"], | |
| "surprise": s | |
| }) | |
| # GDP QoQ | |
| if gdp_data: | |
| gdp_actual = gdp_data[0]['value'] | |
| s = calc_surprise(gdp_actual, CONSENSUS["GDP_QOQ"]) | |
| impact = s['impact'] * (1 if s['direction'] == 'POSITIVE' else -1) | |
| total_score += impact * weights["GDP_QOQ"] | |
| surprises.append({ | |
| "indicator": "GDP_QOQ", | |
| "actual": round(gdp_actual, 2), | |
| "consensus": CONSENSUS["GDP_QOQ"], | |
| "surprise": s | |
| }) | |
| # ISM Manufacturing | |
| if ism_data: | |
| ism_actual = ism_data[0]['value'] | |
| s = calc_surprise(ism_actual, CONSENSUS["ISM_MANUF"]) | |
| impact = s['impact'] * (1 if s['direction'] == 'POSITIVE' else -1) | |
| total_score += impact * weights["ISM_MANUF"] | |
| surprises.append({ | |
| "indicator": "ISM_MANUF", | |
| "actual": round(ism_actual, 2), | |
| "consensus": CONSENSUS["ISM_MANUF"], | |
| "surprise": s | |
| }) | |
| # Retail Sales | |
| if retail_data: | |
| retail_actual = retail_data[0]['value'] | |
| s = calc_surprise(retail_actual, CONSENSUS["RETAIL_SALES"]) | |
| impact = s['impact'] * (1 if s['direction'] == 'POSITIVE' else -1) | |
| total_score += impact * weights["RETAIL_SALES"] | |
| surprises.append({ | |
| "indicator": "RETAIL_SALES", | |
| "actual": round(retail_actual, 2), | |
| "consensus": CONSENSUS["RETAIL_SALES"], | |
| "surprise": s | |
| }) | |
| # FOMC Surprise | |
| if fomc_surprise['impact'] != 0: | |
| total_score += fomc_surprise['impact'] * weights["FOMC"] | |
| surprises.append({ | |
| "indicator": "FOMC", | |
| "signal": fomc_surprise['direction'], | |
| "impact": fomc_surprise['impact'] | |
| }) | |
| # Нормализация | |
| total_score = max(-50, min(50, total_score)) | |
| surprise_index = 50 + total_score | |
| surprise_index = max(0, min(100, surprise_index)) | |
| # Определение режима | |
| if surprise_index > 65: | |
| regime, direction = "RISK_ON", "LONG" | |
| confidence = surprise_index / 100 | |
| elif surprise_index < 35: | |
| regime, direction = "RISK_OFF", "SHORT" | |
| confidence = (100 - surprise_index) / 100 | |
| else: | |
| regime, direction = "NEUTRAL", "WAIT" | |
| confidence = 0.0 | |
| # Сохраняем в историю | |
| SURPRISE_HISTORY.append({ | |
| "timestamp": today.isoformat(), | |
| "surprise_index": round(surprise_index, 2), | |
| "regime": regime, | |
| "surprises": surprises | |
| }) | |
| save_history() | |
| return { | |
| "surprise_index": round(surprise_index, 2), | |
| "market_regime": regime, | |
| "direction": direction, | |
| "confidence": round(confidence, 4), | |
| "surprises": surprises, | |
| "total_score": round(total_score, 2) | |
| } | |
| # ================= ОТПРАВКА В HUB ================= | |
| async def send_signal_to_hub(symbol: str, direction: str, confidence: float): | |
| """Отправка сигнала в Space 17 (Data Hub).""" | |
| try: | |
| resp = await http_client.post(f"{HUB_URL}/signal", json={ | |
| "space": "space_29_macro_surprise", | |
| "symbol": symbol, | |
| "direction": direction, | |
| "confidence": confidence, | |
| "raw": json.dumps({"source": "space_29_macro_surprise"}) | |
| }, 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_macro_surprise_signal() -> Dict[str, Any]: | |
| start = time.time() | |
| analysis = await analyze_macro_surprises() | |
| latency = int((time.time() - start) * 1000) | |
| # Отправка сигналов для всех трёх символов в Hub | |
| for sym in SYMBOLS: | |
| await send_signal_to_hub(sym, analysis['direction'], analysis['confidence']) | |
| result = { | |
| "space": "space_29_macro_surprise", | |
| "timestamp": int(time.time()), | |
| "signals": { | |
| sym: { | |
| "direction": analysis['direction'], | |
| "confidence": analysis['confidence'] | |
| } | |
| for sym in SYMBOLS | |
| }, | |
| "surprise_analysis": analysis, | |
| "latency_ms": latency | |
| } | |
| logger.info(f"📈 Macro Surprise: Index={analysis['surprise_index']:.1f} Regime={analysis['market_regime']}") | |
| return result | |
| # ================= АВТО-ОТПРАВКА ПО ТАЙМЕРУ ================= | |
| async def auto_send_loop(): | |
| """🔥 Фоновая задача: каждый час анализирует макро-сюрпризы и шлёт сигналы в Hub.""" | |
| logger.info(f"🔄 Авто-отправка Macro Surprise запущена (интервал {AUTO_SEND_INTERVAL}с)") | |
| # Первый запуск через 30 секунд после старта | |
| await asyncio.sleep(30) | |
| while True: | |
| try: | |
| logger.info("📈 Macro Surprise авто-анализ...") | |
| await get_macro_surprise_signal() | |
| logger.info("✅ Macro Surprise авто-отправка завершена") | |
| except Exception as e: | |
| logger.error(f"Ошибка в авто-отправке: {e}") | |
| await asyncio.sleep(AUTO_SEND_INTERVAL) | |
| # ================= FASTAPI ================= | |
| app = FastAPI(title="Tomiris Space 29 v2.2 — Macro Surprise Engine (Auto-Hub)") | |
| async def startup(): | |
| # Запускаем фоновую авто-отправку | |
| asyncio.create_task(auto_send_loop()) | |
| logger.info("🚀 Space 29 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, | |
| "indicators": list(CONSENSUS.keys()), | |
| "history_length": len(SURPRISE_HISTORY) | |
| } | |
| async def consilium(): | |
| return await get_macro_surprise_signal() | |
| async def surprise_index(): | |
| return await analyze_macro_surprises() | |
| async def fomc(): | |
| return await fetch_fomc_surprise() | |
| async def history(limit: int = 50): | |
| return list(SURPRISE_HISTORY)[-limit:] | |
| async def send_now(): | |
| """Ручной триггер отправки.""" | |
| return await get_macro_surprise_signal() | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run(app, host="0.0.0.0", port=7860) | |
| print("🚀 SPACE 29 v2.2 — MACRO SURPRISE ENGINE (АВТО-ОТПРАВКА В HUB) ЗАПУЩЕН!") |