Spaces:
Paused
Paused
Update app.py
Browse files
app.py
CHANGED
|
@@ -21,7 +21,7 @@ for module_name, pip_name in REQUIRED_PACKAGES.items():
|
|
| 21 |
print(f"✅ {pip_name} установлен!")
|
| 22 |
|
| 23 |
# ============================================
|
| 24 |
-
# 👑 TOMIRIS SPACE 29 v2.
|
| 25 |
# ============================================
|
| 26 |
import os, time, json, logging, asyncio
|
| 27 |
from typing import Dict, Any, List, Optional
|
|
@@ -41,14 +41,28 @@ HUB_URL = os.getenv("SPACE17_URL", "https://tomiris-ai-name5-5.hf.space")
|
|
| 41 |
FRED_KEY = os.getenv("FRED_KEY", "faa11c8e2e4beee08c5b966e8b63a513")
|
| 42 |
NEWSAPI_KEY = os.getenv("NEWSAPI_KEY", "948c7816beea47baa23b054592472d0e")
|
| 43 |
|
|
|
|
|
|
|
|
|
|
| 44 |
# Консенсус-прогнозы (обновлять ежемесячно)
|
| 45 |
CONSENSUS = {
|
| 46 |
-
"CPI_YOY": 3.2,
|
| 47 |
-
"
|
| 48 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
}
|
| 50 |
|
| 51 |
-
CACHE_TTL = {"fred": 3600, "news": 1800}
|
| 52 |
HISTORY_FILE = "surprise_history.json"
|
| 53 |
|
| 54 |
# ================= HTTP КЛИЕНТ =================
|
|
@@ -77,22 +91,30 @@ async def fetch_fred_series(series_id: str, months: int = 13) -> List[Dict]:
|
|
| 77 |
cache_key = f"fred_{series_id}_{months}"
|
| 78 |
if cache_key in cache_store and time.time() - cache_times.get(cache_key, 0) < CACHE_TTL["fred"]:
|
| 79 |
return cache_store[cache_key]
|
|
|
|
| 80 |
try:
|
| 81 |
r = await http_client.get(
|
| 82 |
-
f"https://api.stlouisfed.org/fred/series/observations?series_id={series_id}&api_key={FRED_KEY}&file_type=json&sort_order=desc&limit={months}"
|
|
|
|
| 83 |
if r.status_code == 200:
|
| 84 |
data = r.json()
|
| 85 |
-
values = [
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
cache_store[cache_key] = values
|
| 87 |
cache_times[cache_key] = time.time()
|
| 88 |
return values
|
| 89 |
except Exception as e:
|
| 90 |
logger.warning(f"FRED {series_id}: {e}")
|
|
|
|
| 91 |
return []
|
| 92 |
|
| 93 |
def get_yoy_change(data: List[Dict], current_month: str) -> Optional[float]:
|
| 94 |
current_val = None
|
| 95 |
prev_val = None
|
|
|
|
| 96 |
for item in data:
|
| 97 |
date = item['date']
|
| 98 |
if date == current_month:
|
|
@@ -100,15 +122,24 @@ def get_yoy_change(data: List[Dict], current_month: str) -> Optional[float]:
|
|
| 100 |
year_ago = str(int(date[:4]) - 1) + date[4:]
|
| 101 |
if date == year_ago and date[:7] == current_month[:7]:
|
| 102 |
prev_val = item['value']
|
|
|
|
| 103 |
if current_val and prev_val:
|
| 104 |
return ((current_val - prev_val) / prev_val) * 100
|
|
|
|
| 105 |
return None
|
| 106 |
|
| 107 |
# ================= АНАЛИЗ СЮРПРИЗА =================
|
| 108 |
def calc_surprise(actual: float, consensus: float) -> Dict:
|
| 109 |
if consensus == 0:
|
| 110 |
-
return {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
surprise_pct = ((actual - consensus) / abs(consensus)) * 100
|
|
|
|
| 112 |
if abs(surprise_pct) > 100:
|
| 113 |
level, impact = "EXTREME_SURPRISE", 30
|
| 114 |
elif abs(surprise_pct) > 50:
|
|
@@ -119,33 +150,73 @@ def calc_surprise(actual: float, consensus: float) -> Dict:
|
|
| 119 |
level, impact = "MINOR_SURPRISE", 5
|
| 120 |
else:
|
| 121 |
level, impact = "IN_LINE", 0
|
|
|
|
| 122 |
direction = "POSITIVE" if surprise_pct > 0 else "NEGATIVE" if surprise_pct < 0 else "NEUTRAL"
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
|
| 125 |
# ================= FOMC SURPRISE (через новости) =================
|
| 126 |
async def fetch_fomc_surprise() -> Dict:
|
| 127 |
if not NEWSAPI_KEY:
|
| 128 |
-
return {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
try:
|
| 130 |
r = await http_client.get(
|
| 131 |
-
f"https://newsapi.org/v2/everything?q=fomc+fed+rate+decision&pageSize=10&apiKey={NEWSAPI_KEY}"
|
|
|
|
| 132 |
if r.status_code == 200:
|
| 133 |
articles = r.json().get('articles', [])
|
| 134 |
-
hawk = sum(
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
except:
|
| 141 |
pass
|
| 142 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
|
| 144 |
# ================= ГЛАВНЫЙ АНАЛИЗ =================
|
| 145 |
async def analyze_macro_surprises() -> Dict:
|
| 146 |
today = datetime.now(timezone.utc)
|
| 147 |
current_month_str = today.strftime("%Y-%m")
|
| 148 |
|
|
|
|
| 149 |
cpi_data = await fetch_fred_series("CPIAUCSL", 13)
|
| 150 |
core_cpi_data = await fetch_fred_series("CPILFESL", 13)
|
| 151 |
unemp_data = await fetch_fred_series("UNRATE", 6)
|
|
@@ -158,51 +229,95 @@ async def analyze_macro_surprises() -> Dict:
|
|
| 158 |
|
| 159 |
surprises = []
|
| 160 |
total_score = 0.0
|
| 161 |
-
weights = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
|
|
|
|
| 163 |
cpi_yoy = get_yoy_change(cpi_data, current_month_str)
|
| 164 |
if cpi_yoy is not None:
|
| 165 |
s = calc_surprise(cpi_yoy, CONSENSUS["CPI_YOY"])
|
| 166 |
impact = s['impact'] * (1 if s['direction'] == 'NEGATIVE' else -0.5)
|
| 167 |
total_score += impact * weights["CPI_YOY"]
|
| 168 |
-
surprises.append({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
|
|
|
|
| 170 |
if nfp_data:
|
| 171 |
nfp_actual = nfp_data[0]['value']
|
| 172 |
s = calc_surprise(nfp_actual, CONSENSUS["NFP"])
|
| 173 |
impact = s['impact'] * (1 if s['direction'] == 'POSITIVE' else -1)
|
| 174 |
total_score += impact * weights["NFP"]
|
| 175 |
-
surprises.append({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
|
|
|
|
| 177 |
if gdp_data:
|
| 178 |
gdp_actual = gdp_data[0]['value']
|
| 179 |
s = calc_surprise(gdp_actual, CONSENSUS["GDP_QOQ"])
|
| 180 |
impact = s['impact'] * (1 if s['direction'] == 'POSITIVE' else -1)
|
| 181 |
total_score += impact * weights["GDP_QOQ"]
|
| 182 |
-
surprises.append({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
|
|
|
|
| 184 |
if ism_data:
|
| 185 |
ism_actual = ism_data[0]['value']
|
| 186 |
s = calc_surprise(ism_actual, CONSENSUS["ISM_MANUF"])
|
| 187 |
impact = s['impact'] * (1 if s['direction'] == 'POSITIVE' else -1)
|
| 188 |
total_score += impact * weights["ISM_MANUF"]
|
| 189 |
-
surprises.append({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
|
|
|
|
| 191 |
if retail_data:
|
| 192 |
retail_actual = retail_data[0]['value']
|
| 193 |
s = calc_surprise(retail_actual, CONSENSUS["RETAIL_SALES"])
|
| 194 |
impact = s['impact'] * (1 if s['direction'] == 'POSITIVE' else -1)
|
| 195 |
total_score += impact * weights["RETAIL_SALES"]
|
| 196 |
-
surprises.append({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
|
|
|
|
| 198 |
if fomc_surprise['impact'] != 0:
|
| 199 |
total_score += fomc_surprise['impact'] * weights["FOMC"]
|
| 200 |
-
surprises.append({
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
|
|
|
|
| 202 |
total_score = max(-50, min(50, total_score))
|
| 203 |
surprise_index = 50 + total_score
|
| 204 |
surprise_index = max(0, min(100, surprise_index))
|
| 205 |
|
|
|
|
| 206 |
if surprise_index > 65:
|
| 207 |
regime, direction = "RISK_ON", "LONG"
|
| 208 |
confidence = surprise_index / 100
|
|
@@ -213,6 +328,7 @@ async def analyze_macro_surprises() -> Dict:
|
|
| 213 |
regime, direction = "NEUTRAL", "WAIT"
|
| 214 |
confidence = 0.0
|
| 215 |
|
|
|
|
| 216 |
SURPRISE_HISTORY.append({
|
| 217 |
"timestamp": today.isoformat(),
|
| 218 |
"surprise_index": round(surprise_index, 2),
|
|
@@ -232,15 +348,19 @@ async def analyze_macro_surprises() -> Dict:
|
|
| 232 |
|
| 233 |
# ================= ОТПРАВКА В HUB =================
|
| 234 |
async def send_signal_to_hub(symbol: str, direction: str, confidence: float):
|
|
|
|
| 235 |
try:
|
| 236 |
-
await http_client.post(f"{HUB_URL}/signal", json={
|
| 237 |
"space": "space_29_macro_surprise",
|
| 238 |
"symbol": symbol,
|
| 239 |
"direction": direction,
|
| 240 |
"confidence": confidence,
|
| 241 |
"raw": json.dumps({"source": "space_29_macro_surprise"})
|
| 242 |
-
})
|
| 243 |
-
|
|
|
|
|
|
|
|
|
|
| 244 |
except Exception as e:
|
| 245 |
logger.error(f"Ошибка отправки в Hub: {e}")
|
| 246 |
|
|
@@ -257,7 +377,13 @@ async def get_macro_surprise_signal() -> Dict[str, Any]:
|
|
| 257 |
result = {
|
| 258 |
"space": "space_29_macro_surprise",
|
| 259 |
"timestamp": int(time.time()),
|
| 260 |
-
"signals": {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
"surprise_analysis": analysis,
|
| 262 |
"latency_ms": latency
|
| 263 |
}
|
|
@@ -265,19 +391,45 @@ async def get_macro_surprise_signal() -> Dict[str, Any]:
|
|
| 265 |
logger.info(f"📈 Macro Surprise: Index={analysis['surprise_index']:.1f} Regime={analysis['market_regime']}")
|
| 266 |
return result
|
| 267 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 268 |
# ================= FASTAPI =================
|
| 269 |
-
app = FastAPI(title="Tomiris Space 29 v2.
|
| 270 |
|
| 271 |
@app.on_event("startup")
|
| 272 |
-
async def startup():
|
|
|
|
|
|
|
|
|
|
| 273 |
|
| 274 |
@app.on_event("shutdown")
|
| 275 |
-
async def shutdown():
|
|
|
|
| 276 |
|
| 277 |
@app.get("/health")
|
| 278 |
async def health():
|
| 279 |
-
return {
|
| 280 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
|
| 282 |
@app.get("/consilium")
|
| 283 |
async def consilium():
|
|
@@ -295,8 +447,13 @@ async def fomc():
|
|
| 295 |
async def history(limit: int = 50):
|
| 296 |
return list(SURPRISE_HISTORY)[-limit:]
|
| 297 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 298 |
if __name__ == "__main__":
|
| 299 |
import uvicorn
|
| 300 |
uvicorn.run(app, host="0.0.0.0", port=7860)
|
| 301 |
|
| 302 |
-
print("🚀 SPACE 29 v2.
|
|
|
|
| 21 |
print(f"✅ {pip_name} установлен!")
|
| 22 |
|
| 23 |
# ============================================
|
| 24 |
+
# 👑 TOMIRIS SPACE 29 v2.2 — MACRO SURPRISE ENGINE (АВТО-ОТПРАВКА В HUB)
|
| 25 |
# ============================================
|
| 26 |
import os, time, json, logging, asyncio
|
| 27 |
from typing import Dict, Any, List, Optional
|
|
|
|
| 41 |
FRED_KEY = os.getenv("FRED_KEY", "faa11c8e2e4beee08c5b966e8b63a513")
|
| 42 |
NEWSAPI_KEY = os.getenv("NEWSAPI_KEY", "948c7816beea47baa23b054592472d0e")
|
| 43 |
|
| 44 |
+
# Интервал авто-отправки (секунды)
|
| 45 |
+
AUTO_SEND_INTERVAL = int(os.getenv("AUTO_SEND_INTERVAL", "3600")) # раз в час — макро-данные медленные
|
| 46 |
+
|
| 47 |
# Консенсус-прогнозы (обновлять ежемесячно)
|
| 48 |
CONSENSUS = {
|
| 49 |
+
"CPI_YOY": 3.2,
|
| 50 |
+
"CORE_CPI_YOY": 3.5,
|
| 51 |
+
"UNEMPLOYMENT": 4.0,
|
| 52 |
+
"NFP": 180000,
|
| 53 |
+
"GDP_QOQ": 2.0,
|
| 54 |
+
"FED_RATE": 4.25,
|
| 55 |
+
"ISM_MANUF": 49.0,
|
| 56 |
+
"ISM_SERVICES": 52.0,
|
| 57 |
+
"RETAIL_SALES": 0.3,
|
| 58 |
+
"DURABLE_GOODS": 0.5
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
CACHE_TTL = {
|
| 62 |
+
"fred": 3600,
|
| 63 |
+
"news": 1800
|
| 64 |
}
|
| 65 |
|
|
|
|
| 66 |
HISTORY_FILE = "surprise_history.json"
|
| 67 |
|
| 68 |
# ================= HTTP КЛИЕНТ =================
|
|
|
|
| 91 |
cache_key = f"fred_{series_id}_{months}"
|
| 92 |
if cache_key in cache_store and time.time() - cache_times.get(cache_key, 0) < CACHE_TTL["fred"]:
|
| 93 |
return cache_store[cache_key]
|
| 94 |
+
|
| 95 |
try:
|
| 96 |
r = await http_client.get(
|
| 97 |
+
f"https://api.stlouisfed.org/fred/series/observations?series_id={series_id}&api_key={FRED_KEY}&file_type=json&sort_order=desc&limit={months}"
|
| 98 |
+
)
|
| 99 |
if r.status_code == 200:
|
| 100 |
data = r.json()
|
| 101 |
+
values = [
|
| 102 |
+
{'date': obs['date'], 'value': float(obs['value'])}
|
| 103 |
+
for obs in data.get('observations', [])
|
| 104 |
+
if obs['value'] != '.'
|
| 105 |
+
]
|
| 106 |
cache_store[cache_key] = values
|
| 107 |
cache_times[cache_key] = time.time()
|
| 108 |
return values
|
| 109 |
except Exception as e:
|
| 110 |
logger.warning(f"FRED {series_id}: {e}")
|
| 111 |
+
|
| 112 |
return []
|
| 113 |
|
| 114 |
def get_yoy_change(data: List[Dict], current_month: str) -> Optional[float]:
|
| 115 |
current_val = None
|
| 116 |
prev_val = None
|
| 117 |
+
|
| 118 |
for item in data:
|
| 119 |
date = item['date']
|
| 120 |
if date == current_month:
|
|
|
|
| 122 |
year_ago = str(int(date[:4]) - 1) + date[4:]
|
| 123 |
if date == year_ago and date[:7] == current_month[:7]:
|
| 124 |
prev_val = item['value']
|
| 125 |
+
|
| 126 |
if current_val and prev_val:
|
| 127 |
return ((current_val - prev_val) / prev_val) * 100
|
| 128 |
+
|
| 129 |
return None
|
| 130 |
|
| 131 |
# ================= АНАЛИЗ СЮРПРИЗА =================
|
| 132 |
def calc_surprise(actual: float, consensus: float) -> Dict:
|
| 133 |
if consensus == 0:
|
| 134 |
+
return {
|
| 135 |
+
"surprise_pct": 0,
|
| 136 |
+
"level": "IN_LINE",
|
| 137 |
+
"impact": 0,
|
| 138 |
+
"direction": "NEUTRAL"
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
surprise_pct = ((actual - consensus) / abs(consensus)) * 100
|
| 142 |
+
|
| 143 |
if abs(surprise_pct) > 100:
|
| 144 |
level, impact = "EXTREME_SURPRISE", 30
|
| 145 |
elif abs(surprise_pct) > 50:
|
|
|
|
| 150 |
level, impact = "MINOR_SURPRISE", 5
|
| 151 |
else:
|
| 152 |
level, impact = "IN_LINE", 0
|
| 153 |
+
|
| 154 |
direction = "POSITIVE" if surprise_pct > 0 else "NEGATIVE" if surprise_pct < 0 else "NEUTRAL"
|
| 155 |
+
|
| 156 |
+
return {
|
| 157 |
+
"surprise_pct": round(surprise_pct, 2),
|
| 158 |
+
"level": level,
|
| 159 |
+
"impact": impact,
|
| 160 |
+
"direction": direction
|
| 161 |
+
}
|
| 162 |
|
| 163 |
# ================= FOMC SURPRISE (через новости) =================
|
| 164 |
async def fetch_fomc_surprise() -> Dict:
|
| 165 |
if not NEWSAPI_KEY:
|
| 166 |
+
return {
|
| 167 |
+
"indicator": "FOMC",
|
| 168 |
+
"impact": 0,
|
| 169 |
+
"direction": "NEUTRAL"
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
try:
|
| 173 |
r = await http_client.get(
|
| 174 |
+
f"https://newsapi.org/v2/everything?q=fomc+fed+rate+decision&pageSize=10&apiKey={NEWSAPI_KEY}"
|
| 175 |
+
)
|
| 176 |
if r.status_code == 200:
|
| 177 |
articles = r.json().get('articles', [])
|
| 178 |
+
hawk = sum(
|
| 179 |
+
1 for a in articles
|
| 180 |
+
if any(w in (a.get('title', '') + a.get('description', '')).lower()
|
| 181 |
+
for w in ['hawkish', 'raise', 'tighten', 'surprise hike'])
|
| 182 |
+
)
|
| 183 |
+
dove = sum(
|
| 184 |
+
1 for a in articles
|
| 185 |
+
if any(w in (a.get('title', '') + a.get('description', '')).lower()
|
| 186 |
+
for w in ['dovish', 'cut', 'ease', 'surprise cut'])
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
if hawk > dove * 2:
|
| 190 |
+
return {
|
| 191 |
+
"indicator": "FOMC",
|
| 192 |
+
"impact": -15,
|
| 193 |
+
"direction": "HAWKISH",
|
| 194 |
+
"hawkish": hawk,
|
| 195 |
+
"dovish": dove
|
| 196 |
+
}
|
| 197 |
+
elif dove > hawk * 2:
|
| 198 |
+
return {
|
| 199 |
+
"indicator": "FOMC",
|
| 200 |
+
"impact": 15,
|
| 201 |
+
"direction": "DOVISH",
|
| 202 |
+
"hawkish": hawk,
|
| 203 |
+
"dovish": dove
|
| 204 |
+
}
|
| 205 |
except:
|
| 206 |
pass
|
| 207 |
+
|
| 208 |
+
return {
|
| 209 |
+
"indicator": "FOMC",
|
| 210 |
+
"impact": 0,
|
| 211 |
+
"direction": "NEUTRAL"
|
| 212 |
+
}
|
| 213 |
|
| 214 |
# ================= ГЛАВНЫЙ АНАЛИЗ =================
|
| 215 |
async def analyze_macro_surprises() -> Dict:
|
| 216 |
today = datetime.now(timezone.utc)
|
| 217 |
current_month_str = today.strftime("%Y-%m")
|
| 218 |
|
| 219 |
+
# Загружаем все данные параллельно
|
| 220 |
cpi_data = await fetch_fred_series("CPIAUCSL", 13)
|
| 221 |
core_cpi_data = await fetch_fred_series("CPILFESL", 13)
|
| 222 |
unemp_data = await fetch_fred_series("UNRATE", 6)
|
|
|
|
| 229 |
|
| 230 |
surprises = []
|
| 231 |
total_score = 0.0
|
| 232 |
+
weights = {
|
| 233 |
+
"CPI_YOY": 0.30,
|
| 234 |
+
"NFP": 0.25,
|
| 235 |
+
"FOMC": 0.20,
|
| 236 |
+
"ISM_MANUF": 0.10,
|
| 237 |
+
"GDP_QOQ": 0.10,
|
| 238 |
+
"RETAIL_SALES": 0.05
|
| 239 |
+
}
|
| 240 |
|
| 241 |
+
# CPI YoY
|
| 242 |
cpi_yoy = get_yoy_change(cpi_data, current_month_str)
|
| 243 |
if cpi_yoy is not None:
|
| 244 |
s = calc_surprise(cpi_yoy, CONSENSUS["CPI_YOY"])
|
| 245 |
impact = s['impact'] * (1 if s['direction'] == 'NEGATIVE' else -0.5)
|
| 246 |
total_score += impact * weights["CPI_YOY"]
|
| 247 |
+
surprises.append({
|
| 248 |
+
"indicator": "CPI_YOY",
|
| 249 |
+
"actual": round(cpi_yoy, 2),
|
| 250 |
+
"consensus": CONSENSUS["CPI_YOY"],
|
| 251 |
+
"surprise": s
|
| 252 |
+
})
|
| 253 |
|
| 254 |
+
# NFP
|
| 255 |
if nfp_data:
|
| 256 |
nfp_actual = nfp_data[0]['value']
|
| 257 |
s = calc_surprise(nfp_actual, CONSENSUS["NFP"])
|
| 258 |
impact = s['impact'] * (1 if s['direction'] == 'POSITIVE' else -1)
|
| 259 |
total_score += impact * weights["NFP"]
|
| 260 |
+
surprises.append({
|
| 261 |
+
"indicator": "NFP",
|
| 262 |
+
"actual": int(nfp_actual),
|
| 263 |
+
"consensus": CONSENSUS["NFP"],
|
| 264 |
+
"surprise": s
|
| 265 |
+
})
|
| 266 |
|
| 267 |
+
# GDP QoQ
|
| 268 |
if gdp_data:
|
| 269 |
gdp_actual = gdp_data[0]['value']
|
| 270 |
s = calc_surprise(gdp_actual, CONSENSUS["GDP_QOQ"])
|
| 271 |
impact = s['impact'] * (1 if s['direction'] == 'POSITIVE' else -1)
|
| 272 |
total_score += impact * weights["GDP_QOQ"]
|
| 273 |
+
surprises.append({
|
| 274 |
+
"indicator": "GDP_QOQ",
|
| 275 |
+
"actual": round(gdp_actual, 2),
|
| 276 |
+
"consensus": CONSENSUS["GDP_QOQ"],
|
| 277 |
+
"surprise": s
|
| 278 |
+
})
|
| 279 |
|
| 280 |
+
# ISM Manufacturing
|
| 281 |
if ism_data:
|
| 282 |
ism_actual = ism_data[0]['value']
|
| 283 |
s = calc_surprise(ism_actual, CONSENSUS["ISM_MANUF"])
|
| 284 |
impact = s['impact'] * (1 if s['direction'] == 'POSITIVE' else -1)
|
| 285 |
total_score += impact * weights["ISM_MANUF"]
|
| 286 |
+
surprises.append({
|
| 287 |
+
"indicator": "ISM_MANUF",
|
| 288 |
+
"actual": round(ism_actual, 2),
|
| 289 |
+
"consensus": CONSENSUS["ISM_MANUF"],
|
| 290 |
+
"surprise": s
|
| 291 |
+
})
|
| 292 |
|
| 293 |
+
# Retail Sales
|
| 294 |
if retail_data:
|
| 295 |
retail_actual = retail_data[0]['value']
|
| 296 |
s = calc_surprise(retail_actual, CONSENSUS["RETAIL_SALES"])
|
| 297 |
impact = s['impact'] * (1 if s['direction'] == 'POSITIVE' else -1)
|
| 298 |
total_score += impact * weights["RETAIL_SALES"]
|
| 299 |
+
surprises.append({
|
| 300 |
+
"indicator": "RETAIL_SALES",
|
| 301 |
+
"actual": round(retail_actual, 2),
|
| 302 |
+
"consensus": CONSENSUS["RETAIL_SALES"],
|
| 303 |
+
"surprise": s
|
| 304 |
+
})
|
| 305 |
|
| 306 |
+
# FOMC Surprise
|
| 307 |
if fomc_surprise['impact'] != 0:
|
| 308 |
total_score += fomc_surprise['impact'] * weights["FOMC"]
|
| 309 |
+
surprises.append({
|
| 310 |
+
"indicator": "FOMC",
|
| 311 |
+
"signal": fomc_surprise['direction'],
|
| 312 |
+
"impact": fomc_surprise['impact']
|
| 313 |
+
})
|
| 314 |
|
| 315 |
+
# Нормализация
|
| 316 |
total_score = max(-50, min(50, total_score))
|
| 317 |
surprise_index = 50 + total_score
|
| 318 |
surprise_index = max(0, min(100, surprise_index))
|
| 319 |
|
| 320 |
+
# Определение режима
|
| 321 |
if surprise_index > 65:
|
| 322 |
regime, direction = "RISK_ON", "LONG"
|
| 323 |
confidence = surprise_index / 100
|
|
|
|
| 328 |
regime, direction = "NEUTRAL", "WAIT"
|
| 329 |
confidence = 0.0
|
| 330 |
|
| 331 |
+
# Сохраняем в историю
|
| 332 |
SURPRISE_HISTORY.append({
|
| 333 |
"timestamp": today.isoformat(),
|
| 334 |
"surprise_index": round(surprise_index, 2),
|
|
|
|
| 348 |
|
| 349 |
# ================= ОТПРАВКА В HUB =================
|
| 350 |
async def send_signal_to_hub(symbol: str, direction: str, confidence: float):
|
| 351 |
+
"""Отправка сигнала в Space 17 (Data Hub)."""
|
| 352 |
try:
|
| 353 |
+
resp = await http_client.post(f"{HUB_URL}/signal", json={
|
| 354 |
"space": "space_29_macro_surprise",
|
| 355 |
"symbol": symbol,
|
| 356 |
"direction": direction,
|
| 357 |
"confidence": confidence,
|
| 358 |
"raw": json.dumps({"source": "space_29_macro_surprise"})
|
| 359 |
+
}, timeout=10)
|
| 360 |
+
if resp.status_code == 200:
|
| 361 |
+
logger.info(f"📤 {symbol}: {direction} conf={confidence:.3f} отправлен в Hub")
|
| 362 |
+
else:
|
| 363 |
+
logger.warning(f"Hub вернул {resp.status_code}: {resp.text[:100]}")
|
| 364 |
except Exception as e:
|
| 365 |
logger.error(f"Ошибка отправки в Hub: {e}")
|
| 366 |
|
|
|
|
| 377 |
result = {
|
| 378 |
"space": "space_29_macro_surprise",
|
| 379 |
"timestamp": int(time.time()),
|
| 380 |
+
"signals": {
|
| 381 |
+
sym: {
|
| 382 |
+
"direction": analysis['direction'],
|
| 383 |
+
"confidence": analysis['confidence']
|
| 384 |
+
}
|
| 385 |
+
for sym in SYMBOLS
|
| 386 |
+
},
|
| 387 |
"surprise_analysis": analysis,
|
| 388 |
"latency_ms": latency
|
| 389 |
}
|
|
|
|
| 391 |
logger.info(f"📈 Macro Surprise: Index={analysis['surprise_index']:.1f} Regime={analysis['market_regime']}")
|
| 392 |
return result
|
| 393 |
|
| 394 |
+
# ================= АВТО-ОТПРАВКА ПО ТАЙМЕРУ =================
|
| 395 |
+
async def auto_send_loop():
|
| 396 |
+
"""🔥 Фоновая задача: каждый час анализирует макро-сюрпризы и шлёт сигналы в Hub."""
|
| 397 |
+
logger.info(f"🔄 Авто-отправка Macro Surprise запущена (интервал {AUTO_SEND_INTERVAL}с)")
|
| 398 |
+
# Первый запуск через 30 секунд после старта
|
| 399 |
+
await asyncio.sleep(30)
|
| 400 |
+
while True:
|
| 401 |
+
try:
|
| 402 |
+
logger.info("📈 Macro Surprise авто-анализ...")
|
| 403 |
+
await get_macro_surprise_signal()
|
| 404 |
+
logger.info("✅ Macro Surprise авто-отправка завершена")
|
| 405 |
+
except Exception as e:
|
| 406 |
+
logger.error(f"Ошибка в авто-отправке: {e}")
|
| 407 |
+
|
| 408 |
+
await asyncio.sleep(AUTO_SEND_INTERVAL)
|
| 409 |
+
|
| 410 |
# ================= FASTAPI =================
|
| 411 |
+
app = FastAPI(title="Tomiris Space 29 v2.2 — Macro Surprise Engine (Auto-Hub)")
|
| 412 |
|
| 413 |
@app.on_event("startup")
|
| 414 |
+
async def startup():
|
| 415 |
+
# Запускаем ф��новую авто-отправку
|
| 416 |
+
asyncio.create_task(auto_send_loop())
|
| 417 |
+
logger.info("🚀 Space 29 v2.2 запущен с авто-отправкой в Hub")
|
| 418 |
|
| 419 |
@app.on_event("shutdown")
|
| 420 |
+
async def shutdown():
|
| 421 |
+
await http_client.aclose()
|
| 422 |
|
| 423 |
@app.get("/health")
|
| 424 |
async def health():
|
| 425 |
+
return {
|
| 426 |
+
"status": "operational",
|
| 427 |
+
"version": "2.2",
|
| 428 |
+
"hub_url": HUB_URL,
|
| 429 |
+
"auto_send_interval": AUTO_SEND_INTERVAL,
|
| 430 |
+
"indicators": list(CONSENSUS.keys()),
|
| 431 |
+
"history_length": len(SURPRISE_HISTORY)
|
| 432 |
+
}
|
| 433 |
|
| 434 |
@app.get("/consilium")
|
| 435 |
async def consilium():
|
|
|
|
| 447 |
async def history(limit: int = 50):
|
| 448 |
return list(SURPRISE_HISTORY)[-limit:]
|
| 449 |
|
| 450 |
+
@app.get("/send_now")
|
| 451 |
+
async def send_now():
|
| 452 |
+
"""Ручной триггер отправки."""
|
| 453 |
+
return await get_macro_surprise_signal()
|
| 454 |
+
|
| 455 |
if __name__ == "__main__":
|
| 456 |
import uvicorn
|
| 457 |
uvicorn.run(app, host="0.0.0.0", port=7860)
|
| 458 |
|
| 459 |
+
print("🚀 SPACE 29 v2.2 — MACRO SURPRISE ENGINE (АВТО-ОТПРАВКА В HUB) ЗАПУЩЕН!")
|