TomirisGold5 / app.py
tomirisg25's picture
Update app.py
7b272e6 verified
Raw
History Blame Contribute Delete
15.7 kB
# ============================================
# АВТО-УСТАНОВКА ПАКЕТОВ
# ============================================
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)")
@app.on_event("startup")
async def startup():
# Запускаем фоновую авто-отправку
asyncio.create_task(auto_send_loop())
logger.info("🚀 Space 29 v2.2 запущен с авто-отправкой в Hub")
@app.on_event("shutdown")
async def shutdown():
await http_client.aclose()
@app.get("/health")
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)
}
@app.get("/consilium")
async def consilium():
return await get_macro_surprise_signal()
@app.get("/surprise_index")
async def surprise_index():
return await analyze_macro_surprises()
@app.get("/fomc")
async def fomc():
return await fetch_fomc_surprise()
@app.get("/history")
async def history(limit: int = 50):
return list(SURPRISE_HISTORY)[-limit:]
@app.get("/send_now")
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) ЗАПУЩЕН!")