Spaces:
Sleeping
Sleeping
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 26 v2.
|
| 25 |
# ============================================
|
| 26 |
import os, time, json, logging, asyncio
|
| 27 |
from typing import Dict, Any, List, Optional
|
|
@@ -41,6 +41,9 @@ HUB_URL = os.getenv("SPACE17_URL", "https://tomiris-ai-name5-5.hf.space")
|
|
| 41 |
TWELVE_DATA_KEY = os.getenv("TWELVE_DATA_KEY", "58e67e0008e24161ac9b1671b7c2d2d0")
|
| 42 |
FRED_KEY = os.getenv("FRED_KEY", "faa11c8e2e4beee08c5b966e8b63a513")
|
| 43 |
|
|
|
|
|
|
|
|
|
|
| 44 |
HISTORY_FILE = "options_history.json"
|
| 45 |
CACHE_TTL = {
|
| 46 |
"funding": 60, "oi": 60, "lsr": 120, "deribit": 300,
|
|
@@ -66,40 +69,31 @@ def breaker_open(name: str) -> bool:
|
|
| 66 |
|
| 67 |
def breaker_record(name: str, success: bool):
|
| 68 |
info = CIRCUIT_BREAKER.get(name, {"fails": 0, "last_fail": 0})
|
| 69 |
-
if success:
|
| 70 |
-
|
| 71 |
-
else:
|
| 72 |
-
info["fails"] += 1
|
| 73 |
-
info["last_fail"] = time.time()
|
| 74 |
CIRCUIT_BREAKER[name] = info
|
| 75 |
|
| 76 |
if os.path.exists(HISTORY_FILE):
|
| 77 |
try:
|
| 78 |
-
with open(HISTORY_FILE) as f:
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
OPTIONS_HISTORY = deque(maxlen=500)
|
| 82 |
-
else:
|
| 83 |
-
OPTIONS_HISTORY = deque(maxlen=500)
|
| 84 |
|
| 85 |
def save_history():
|
| 86 |
-
with open(HISTORY_FILE, 'w') as f:
|
| 87 |
-
json.dump(list(OPTIONS_HISTORY), f)
|
| 88 |
|
| 89 |
-
# ================= ЗАГРУЗКА ДАННЫХ
|
| 90 |
async def fetch_vix() -> Dict[str, Any]:
|
| 91 |
if breaker_open("vix"): return {"vix": 20.0, "level": "NORMAL", "signal": "NEUTRAL"}
|
| 92 |
try:
|
| 93 |
r = await http_client.get(f"https://api.twelvedata.com/quote?symbol=VIX&apikey={TWELVE_DATA_KEY}")
|
| 94 |
if r.status_code == 200:
|
| 95 |
-
data = r.json()
|
| 96 |
-
vix_val = float(data.get("close", 20))
|
| 97 |
level = "HIGH" if vix_val > 30 else "ELEVATED" if vix_val > 25 else "NORMAL"
|
| 98 |
signal = "BEARISH_RISK" if vix_val > 30 else "NEUTRAL"
|
| 99 |
breaker_record("vix", True)
|
| 100 |
return {"vix": vix_val, "level": level, "signal": signal}
|
| 101 |
-
except:
|
| 102 |
-
breaker_record("vix", False)
|
| 103 |
return {"vix": 20.0, "level": "NORMAL", "signal": "NEUTRAL"}
|
| 104 |
|
| 105 |
async def fetch_funding_rate(symbol: str) -> Dict[str, Any]:
|
|
@@ -113,8 +107,7 @@ async def fetch_funding_rate(symbol: str) -> Dict[str, Any]:
|
|
| 113 |
signal = "BEARISH" if fr > 0.001 else "BULLISH" if fr < -0.001 else "NEUTRAL"
|
| 114 |
breaker_record(f"funding_{symbol}", True)
|
| 115 |
return {"funding_rate": fr, "funding_rate_pct": round(fr * 100, 4), "signal": signal}
|
| 116 |
-
except:
|
| 117 |
-
breaker_record(f"funding_{symbol}", False)
|
| 118 |
return {"funding_rate": 0, "signal": "NEUTRAL"}
|
| 119 |
|
| 120 |
async def fetch_open_interest(symbol: str) -> Dict[str, Any]:
|
|
@@ -123,15 +116,13 @@ async def fetch_open_interest(symbol: str) -> Dict[str, Any]:
|
|
| 123 |
r = await http_client.get(f"https://fapi.binance.com/fapi/v1/openInterest?symbol={symbol}")
|
| 124 |
if r.status_code == 200:
|
| 125 |
oi = float(r.json().get("openInterest", 0))
|
| 126 |
-
prev_key = f"oi_{symbol}_prev"
|
| 127 |
-
prev_oi = cache_store.get(prev_key, oi)
|
| 128 |
change = ((oi - prev_oi) / prev_oi * 100) if prev_oi > 0 else 0
|
| 129 |
cache_store[prev_key] = oi
|
| 130 |
signal = "BULLISH" if change > 3 else "BEARISH" if change < -3 else "NEUTRAL"
|
| 131 |
breaker_record(f"oi_{symbol}", True)
|
| 132 |
return {"open_interest": oi, "change_pct": round(change, 2), "signal": signal}
|
| 133 |
-
except:
|
| 134 |
-
breaker_record(f"oi_{symbol}", False)
|
| 135 |
return {"open_interest": 0, "change_pct": 0, "signal": "NEUTRAL"}
|
| 136 |
|
| 137 |
async def fetch_long_short_ratio(symbol: str) -> Dict[str, Any]:
|
|
@@ -140,15 +131,13 @@ async def fetch_long_short_ratio(symbol: str) -> Dict[str, Any]:
|
|
| 140 |
r = await http_client.get(f"https://fapi.binance.com/fapi/v1/globalLongShortAccountRatio?symbol={symbol}&period=5m")
|
| 141 |
if r.status_code == 200:
|
| 142 |
lsr = float(r.json().get("longShortRatio", 1))
|
| 143 |
-
long_pct = lsr / (1 + lsr) * 100
|
| 144 |
-
short_pct = 100 - long_pct
|
| 145 |
if lsr > 2.0: signal = "BEARISH"
|
| 146 |
elif lsr < 0.5: signal = "BULLISH"
|
| 147 |
else: signal = "NEUTRAL"
|
| 148 |
breaker_record(f"lsr_{symbol}", True)
|
| 149 |
return {"long_short_ratio": round(lsr, 4), "long_pct": round(long_pct, 1), "short_pct": round(short_pct, 1), "signal": signal}
|
| 150 |
-
except:
|
| 151 |
-
breaker_record(f"lsr_{symbol}", False)
|
| 152 |
return {"long_short_ratio": 1, "signal": "NEUTRAL"}
|
| 153 |
|
| 154 |
async def fetch_deribit_options(coin: str = "ETH") -> Dict[str, Any]:
|
|
@@ -163,8 +152,7 @@ async def fetch_deribit_options(coin: str = "ETH") -> Dict[str, Any]:
|
|
| 163 |
signal = "BEARISH" if pcr > 1.3 else "BULLISH" if pcr < 0.7 else "NEUTRAL"
|
| 164 |
breaker_record("deribit", True)
|
| 165 |
return {"put_call_ratio_volume": round(pcr, 4), "signal": signal}
|
| 166 |
-
except:
|
| 167 |
-
breaker_record("deribit", False)
|
| 168 |
return {"put_call_ratio_volume": 1.0, "signal": "NEUTRAL"}
|
| 169 |
|
| 170 |
async def fetch_gold_derivatives() -> Dict[str, Any]:
|
|
@@ -179,24 +167,19 @@ async def fetch_gold_derivatives() -> Dict[str, Any]:
|
|
| 179 |
signal = "BULLISH" if dxy_change < -2 and tips_current < 0 else "BEARISH" if dxy_change > 2 and tips_current > 0 else "NEUTRAL"
|
| 180 |
breaker_record("fred", True)
|
| 181 |
return {"dxy": dxy_vals[0] if dxy_vals else 104.5, "dxy_change_pct": round(dxy_change, 2), "tips_yield": tips_current, "signal": signal}
|
| 182 |
-
except:
|
| 183 |
-
breaker_record("fred", False)
|
| 184 |
return {"signal": "NEUTRAL"}
|
| 185 |
|
| 186 |
async def get_current_price(symbol: str) -> float:
|
| 187 |
try:
|
| 188 |
r = await http_client.get(f"{HUB_URL}/price/{symbol}")
|
| 189 |
-
if r.status_code == 200:
|
| 190 |
-
|
| 191 |
-
except:
|
| 192 |
-
pass
|
| 193 |
bin_symbol = symbol.replace("/", "")
|
| 194 |
try:
|
| 195 |
r = await http_client.get(f"https://fapi.binance.com/fapi/v1/ticker/price?symbol={bin_symbol}")
|
| 196 |
-
if r.status_code == 200:
|
| 197 |
-
|
| 198 |
-
except:
|
| 199 |
-
pass
|
| 200 |
return 0.0
|
| 201 |
|
| 202 |
def calculate_max_pain(current_price: float) -> float:
|
|
@@ -208,101 +191,71 @@ async def analyze_derivatives(symbol: str) -> Dict[str, Any]:
|
|
| 208 |
bin_sym = "" if native == "XAU" else (native + "USDT")
|
| 209 |
|
| 210 |
tasks = []
|
| 211 |
-
if bin_sym:
|
| 212 |
-
|
| 213 |
-
if native == "
|
| 214 |
-
tasks.append(fetch_deribit_options("ETH"))
|
| 215 |
-
if native == "XAU":
|
| 216 |
-
tasks.append(fetch_gold_derivatives())
|
| 217 |
tasks.append(fetch_vix())
|
| 218 |
|
| 219 |
results = await asyncio.gather(*tasks)
|
| 220 |
-
idx = 0
|
| 221 |
-
metrics = {}
|
| 222 |
if bin_sym:
|
| 223 |
metrics['funding_rate'] = results[idx]; idx += 1
|
| 224 |
metrics['open_interest'] = results[idx]; idx += 1
|
| 225 |
metrics['long_short_ratio'] = results[idx]; idx += 1
|
| 226 |
-
if native == "ETH":
|
| 227 |
-
|
| 228 |
-
if native == "XAU":
|
| 229 |
-
metrics['gold_derivatives'] = results[idx]; idx += 1
|
| 230 |
vix_data = results[idx]
|
| 231 |
|
| 232 |
-
signals = []
|
| 233 |
-
score = 50.0
|
| 234 |
|
| 235 |
if vix_data["vix"] > 30:
|
| 236 |
signals.append({"source": "VIX", "signal": "BEARISH_RISK", "reason": f"VIX={vix_data['vix']:.1f}"})
|
| 237 |
score += 15 if native == "XAU" else -10
|
| 238 |
-
elif vix_data["vix"] > 25:
|
| 239 |
-
score += 5
|
| 240 |
|
| 241 |
fund = metrics.get('funding_rate', {})
|
| 242 |
if fund.get('signal') == 'BEARISH':
|
| 243 |
-
signals.append({"source": "FUNDING", "signal": "BEARISH", "reason": f"Перегретый лонг ({fund.get('funding_rate_pct')}%)"})
|
| 244 |
-
score -= 15
|
| 245 |
elif fund.get('signal') == 'BULLISH':
|
| 246 |
-
signals.append({"source": "FUNDING", "signal": "BULLISH", "reason": f"Перегретый шорт ({fund.get('funding_rate_pct')}%)"})
|
| 247 |
-
score += 15
|
| 248 |
|
| 249 |
oi = metrics.get('open_interest', {})
|
| 250 |
if oi.get('signal') == 'BEARISH':
|
| 251 |
-
signals.append({"source": "OI", "signal": "BEARISH", "reason": f"OI перегрет ({oi.get('change_pct')}%)"})
|
| 252 |
-
score -= 10
|
| 253 |
elif oi.get('signal') == 'BULLISH':
|
| 254 |
-
signals.append({"source": "OI", "signal": "BULLISH", "reason": f"OI растёт ({oi.get('change_pct')}%)"})
|
| 255 |
-
score += 10
|
| 256 |
|
| 257 |
lsr = metrics.get('long_short_ratio', {})
|
| 258 |
if lsr.get('signal') == 'BEARISH':
|
| 259 |
-
signals.append({"source": "L/S", "signal": "BEARISH", "reason": f"Слишком много лонгов ({lsr.get('long_pct')}%)"})
|
| 260 |
-
score -= 10
|
| 261 |
elif lsr.get('signal') == 'BULLISH':
|
| 262 |
-
signals.append({"source": "L/S", "signal": "BULLISH", "reason": f"Много шортов ({lsr.get('short_pct')}%)"})
|
| 263 |
-
score += 10
|
| 264 |
|
| 265 |
options = metrics.get('options', {})
|
| 266 |
if options.get('signal') == 'BEARISH':
|
| 267 |
-
signals.append({"source": "OPTIONS", "signal": "BEARISH", "reason": f"PCR={options.get('put_call_ratio_volume')}"})
|
| 268 |
-
score -= 8
|
| 269 |
elif options.get('signal') == 'BULLISH':
|
| 270 |
-
signals.append({"source": "OPTIONS", "signal": "BULLISH", "reason": f"PCR={options.get('put_call_ratio_volume')}"})
|
| 271 |
-
score += 8
|
| 272 |
|
| 273 |
gold = metrics.get('gold_derivatives', {})
|
| 274 |
if gold.get('signal') == 'BULLISH':
|
| 275 |
-
signals.append({"source": "COT/DXY/TIPS", "signal": "BULLISH", "reason": "Макро-факторы за золото"})
|
| 276 |
-
score += 12
|
| 277 |
elif gold.get('signal') == 'BEARISH':
|
| 278 |
-
signals.append({"source": "COT/DXY/TIPS", "signal": "BEARISH", "reason": "Макро-факторы против золота"})
|
| 279 |
-
score -= 12
|
| 280 |
|
| 281 |
price = await get_current_price(symbol) if native != "XAU" else 0
|
| 282 |
max_pain = calculate_max_pain(price) if price > 0 else 0
|
| 283 |
if max_pain > 0 and price < max_pain:
|
| 284 |
-
signals.append({"source": "MAX_PAIN", "signal": "BULLISH", "reason": f"Цена ниже Max Pain ({max_pain})"})
|
| 285 |
-
score += 8
|
| 286 |
elif max_pain > 0 and price > max_pain:
|
| 287 |
-
signals.append({"source": "MAX_PAIN", "signal": "BEARISH", "reason": f"Цена выше Max Pain ({max_pain})"})
|
| 288 |
-
score -= 8
|
| 289 |
|
| 290 |
score = max(0, min(100, score))
|
| 291 |
-
if score > 60:
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
direction, confidence = "SHORT", (100 - score) / 100
|
| 295 |
-
else:
|
| 296 |
-
direction, confidence = "WAIT", 0.0
|
| 297 |
-
|
| 298 |
-
analysis = {
|
| 299 |
-
"derivative_score": score,
|
| 300 |
-
"direction": direction,
|
| 301 |
-
"confidence": round(confidence, 4),
|
| 302 |
-
"signals": signals,
|
| 303 |
-
"metrics": {**metrics, "vix": vix_data, "max_pain": max_pain}
|
| 304 |
-
}
|
| 305 |
|
|
|
|
| 306 |
OPTIONS_HISTORY.append({"timestamp": datetime.now(timezone.utc).isoformat(), "symbol": symbol, "direction": direction, "score": score})
|
| 307 |
save_history()
|
| 308 |
return analysis
|
|
@@ -310,50 +263,50 @@ async def analyze_derivatives(symbol: str) -> Dict[str, Any]:
|
|
| 310 |
# ================= ОТПРАВКА В HUB =================
|
| 311 |
async def send_signal_to_hub(symbol: str, direction: str, confidence: float):
|
| 312 |
try:
|
| 313 |
-
await http_client.post(f"{HUB_URL}/signal", json={
|
| 314 |
-
"space": "space_26_options",
|
| 315 |
-
"
|
| 316 |
-
"direction": direction,
|
| 317 |
-
"confidence": confidence,
|
| 318 |
"raw": json.dumps({"source": "space_26_options"})
|
| 319 |
-
})
|
| 320 |
-
logger.info(f"📤 {symbol}: {direction} conf={confidence:.3f} отправлен в Hub")
|
| 321 |
-
|
| 322 |
-
|
| 323 |
|
| 324 |
# ================= ГЛАВНЫЙ СИГНАЛ =================
|
| 325 |
async def get_derivative_signal(symbol: str = "ETH/USD") -> Dict[str, Any]:
|
| 326 |
-
start = time.time()
|
| 327 |
-
analysis = await analyze_derivatives(symbol)
|
| 328 |
latency = int((time.time() - start) * 1000)
|
| 329 |
-
|
| 330 |
-
# Отправка в Hub
|
| 331 |
await send_signal_to_hub(symbol, analysis['direction'], analysis['confidence'])
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
"space": "space_26_options",
|
| 335 |
-
"timestamp": int(time.time()),
|
| 336 |
-
"symbol": symbol,
|
| 337 |
-
"signal": {"direction": analysis['direction'], "confidence": analysis['confidence']},
|
| 338 |
-
"derivative_analysis": analysis,
|
| 339 |
-
"latency_ms": latency
|
| 340 |
-
}
|
| 341 |
-
|
| 342 |
-
logger.info(f"📊 Options {symbol}: {analysis['direction']} conf={analysis['confidence']:.3f} score={analysis['derivative_score']}")
|
| 343 |
return result
|
| 344 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
# ================= FASTAPI =================
|
| 346 |
-
app = FastAPI(title="Tomiris Space 26 v2.
|
| 347 |
|
| 348 |
@app.on_event("startup")
|
| 349 |
-
async def startup():
|
|
|
|
|
|
|
| 350 |
|
| 351 |
@app.on_event("shutdown")
|
| 352 |
async def shutdown(): await http_client.aclose()
|
| 353 |
|
| 354 |
@app.get("/health")
|
| 355 |
async def health():
|
| 356 |
-
return {"status": "operational", "version": "2.
|
| 357 |
|
| 358 |
@app.get("/consilium")
|
| 359 |
async def consilium(symbol: str = Query("ETH/USD")):
|
|
@@ -362,33 +315,32 @@ async def consilium(symbol: str = Query("ETH/USD")):
|
|
| 362 |
|
| 363 |
@app.get("/funding/{symbol}")
|
| 364 |
async def funding(symbol: str): return await fetch_funding_rate(symbol.upper())
|
| 365 |
-
|
| 366 |
@app.get("/oi/{symbol}")
|
| 367 |
async def oi(symbol: str): return await fetch_open_interest(symbol.upper())
|
| 368 |
-
|
| 369 |
@app.get("/lsr/{symbol}")
|
| 370 |
async def lsr(symbol: str): return await fetch_long_short_ratio(symbol.upper())
|
| 371 |
-
|
| 372 |
@app.get("/options")
|
| 373 |
async def options(): return await fetch_deribit_options("ETH")
|
| 374 |
-
|
| 375 |
@app.get("/gold")
|
| 376 |
async def gold(): return await fetch_gold_derivatives()
|
| 377 |
-
|
| 378 |
@app.get("/vix")
|
| 379 |
async def vix(): return await fetch_vix()
|
| 380 |
-
|
| 381 |
@app.get("/maxpain/{symbol}")
|
| 382 |
async def maxpain(symbol: str):
|
| 383 |
-
price = await get_current_price(symbol)
|
| 384 |
-
mp = calculate_max_pain(price)
|
| 385 |
return {"symbol": symbol, "current_price": price, "max_pain": mp}
|
| 386 |
-
|
| 387 |
@app.get("/history")
|
| 388 |
async def history(limit: int = 50): return list(OPTIONS_HISTORY)[-limit:]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 389 |
|
| 390 |
if __name__ == "__main__":
|
| 391 |
import uvicorn
|
| 392 |
uvicorn.run(app, host="0.0.0.0", port=7860)
|
| 393 |
|
| 394 |
-
print("🚀 SPACE 26 v2.
|
|
|
|
| 21 |
print(f"✅ {pip_name} установлен!")
|
| 22 |
|
| 23 |
# ============================================
|
| 24 |
+
# 👑 TOMIRIS SPACE 26 v2.4 — OPTIONS & DERIVATIVES ENGINE (АВТО-ОТПРАВКА)
|
| 25 |
# ============================================
|
| 26 |
import os, time, json, logging, asyncio
|
| 27 |
from typing import Dict, Any, List, Optional
|
|
|
|
| 41 |
TWELVE_DATA_KEY = os.getenv("TWELVE_DATA_KEY", "58e67e0008e24161ac9b1671b7c2d2d0")
|
| 42 |
FRED_KEY = os.getenv("FRED_KEY", "faa11c8e2e4beee08c5b966e8b63a513")
|
| 43 |
|
| 44 |
+
# Интервал авто-отправки
|
| 45 |
+
AUTO_SEND_INTERVAL = int(os.getenv("AUTO_SEND_INTERVAL", "300"))
|
| 46 |
+
|
| 47 |
HISTORY_FILE = "options_history.json"
|
| 48 |
CACHE_TTL = {
|
| 49 |
"funding": 60, "oi": 60, "lsr": 120, "deribit": 300,
|
|
|
|
| 69 |
|
| 70 |
def breaker_record(name: str, success: bool):
|
| 71 |
info = CIRCUIT_BREAKER.get(name, {"fails": 0, "last_fail": 0})
|
| 72 |
+
if success: info["fails"] = 0
|
| 73 |
+
else: info["fails"] += 1; info["last_fail"] = time.time()
|
|
|
|
|
|
|
|
|
|
| 74 |
CIRCUIT_BREAKER[name] = info
|
| 75 |
|
| 76 |
if os.path.exists(HISTORY_FILE):
|
| 77 |
try:
|
| 78 |
+
with open(HISTORY_FILE) as f: OPTIONS_HISTORY = deque(json.load(f), maxlen=500)
|
| 79 |
+
except: OPTIONS_HISTORY = deque(maxlen=500)
|
| 80 |
+
else: OPTIONS_HISTORY = deque(maxlen=500)
|
|
|
|
|
|
|
|
|
|
| 81 |
|
| 82 |
def save_history():
|
| 83 |
+
with open(HISTORY_FILE, 'w') as f: json.dump(list(OPTIONS_HISTORY), f)
|
|
|
|
| 84 |
|
| 85 |
+
# ================= ЗАГРУЗКА ДАННЫХ =================
|
| 86 |
async def fetch_vix() -> Dict[str, Any]:
|
| 87 |
if breaker_open("vix"): return {"vix": 20.0, "level": "NORMAL", "signal": "NEUTRAL"}
|
| 88 |
try:
|
| 89 |
r = await http_client.get(f"https://api.twelvedata.com/quote?symbol=VIX&apikey={TWELVE_DATA_KEY}")
|
| 90 |
if r.status_code == 200:
|
| 91 |
+
data = r.json(); vix_val = float(data.get("close", 20))
|
|
|
|
| 92 |
level = "HIGH" if vix_val > 30 else "ELEVATED" if vix_val > 25 else "NORMAL"
|
| 93 |
signal = "BEARISH_RISK" if vix_val > 30 else "NEUTRAL"
|
| 94 |
breaker_record("vix", True)
|
| 95 |
return {"vix": vix_val, "level": level, "signal": signal}
|
| 96 |
+
except: breaker_record("vix", False)
|
|
|
|
| 97 |
return {"vix": 20.0, "level": "NORMAL", "signal": "NEUTRAL"}
|
| 98 |
|
| 99 |
async def fetch_funding_rate(symbol: str) -> Dict[str, Any]:
|
|
|
|
| 107 |
signal = "BEARISH" if fr > 0.001 else "BULLISH" if fr < -0.001 else "NEUTRAL"
|
| 108 |
breaker_record(f"funding_{symbol}", True)
|
| 109 |
return {"funding_rate": fr, "funding_rate_pct": round(fr * 100, 4), "signal": signal}
|
| 110 |
+
except: breaker_record(f"funding_{symbol}", False)
|
|
|
|
| 111 |
return {"funding_rate": 0, "signal": "NEUTRAL"}
|
| 112 |
|
| 113 |
async def fetch_open_interest(symbol: str) -> Dict[str, Any]:
|
|
|
|
| 116 |
r = await http_client.get(f"https://fapi.binance.com/fapi/v1/openInterest?symbol={symbol}")
|
| 117 |
if r.status_code == 200:
|
| 118 |
oi = float(r.json().get("openInterest", 0))
|
| 119 |
+
prev_key = f"oi_{symbol}_prev"; prev_oi = cache_store.get(prev_key, oi)
|
|
|
|
| 120 |
change = ((oi - prev_oi) / prev_oi * 100) if prev_oi > 0 else 0
|
| 121 |
cache_store[prev_key] = oi
|
| 122 |
signal = "BULLISH" if change > 3 else "BEARISH" if change < -3 else "NEUTRAL"
|
| 123 |
breaker_record(f"oi_{symbol}", True)
|
| 124 |
return {"open_interest": oi, "change_pct": round(change, 2), "signal": signal}
|
| 125 |
+
except: breaker_record(f"oi_{symbol}", False)
|
|
|
|
| 126 |
return {"open_interest": 0, "change_pct": 0, "signal": "NEUTRAL"}
|
| 127 |
|
| 128 |
async def fetch_long_short_ratio(symbol: str) -> Dict[str, Any]:
|
|
|
|
| 131 |
r = await http_client.get(f"https://fapi.binance.com/fapi/v1/globalLongShortAccountRatio?symbol={symbol}&period=5m")
|
| 132 |
if r.status_code == 200:
|
| 133 |
lsr = float(r.json().get("longShortRatio", 1))
|
| 134 |
+
long_pct = lsr / (1 + lsr) * 100; short_pct = 100 - long_pct
|
|
|
|
| 135 |
if lsr > 2.0: signal = "BEARISH"
|
| 136 |
elif lsr < 0.5: signal = "BULLISH"
|
| 137 |
else: signal = "NEUTRAL"
|
| 138 |
breaker_record(f"lsr_{symbol}", True)
|
| 139 |
return {"long_short_ratio": round(lsr, 4), "long_pct": round(long_pct, 1), "short_pct": round(short_pct, 1), "signal": signal}
|
| 140 |
+
except: breaker_record(f"lsr_{symbol}", False)
|
|
|
|
| 141 |
return {"long_short_ratio": 1, "signal": "NEUTRAL"}
|
| 142 |
|
| 143 |
async def fetch_deribit_options(coin: str = "ETH") -> Dict[str, Any]:
|
|
|
|
| 152 |
signal = "BEARISH" if pcr > 1.3 else "BULLISH" if pcr < 0.7 else "NEUTRAL"
|
| 153 |
breaker_record("deribit", True)
|
| 154 |
return {"put_call_ratio_volume": round(pcr, 4), "signal": signal}
|
| 155 |
+
except: breaker_record("deribit", False)
|
|
|
|
| 156 |
return {"put_call_ratio_volume": 1.0, "signal": "NEUTRAL"}
|
| 157 |
|
| 158 |
async def fetch_gold_derivatives() -> Dict[str, Any]:
|
|
|
|
| 167 |
signal = "BULLISH" if dxy_change < -2 and tips_current < 0 else "BEARISH" if dxy_change > 2 and tips_current > 0 else "NEUTRAL"
|
| 168 |
breaker_record("fred", True)
|
| 169 |
return {"dxy": dxy_vals[0] if dxy_vals else 104.5, "dxy_change_pct": round(dxy_change, 2), "tips_yield": tips_current, "signal": signal}
|
| 170 |
+
except: breaker_record("fred", False)
|
|
|
|
| 171 |
return {"signal": "NEUTRAL"}
|
| 172 |
|
| 173 |
async def get_current_price(symbol: str) -> float:
|
| 174 |
try:
|
| 175 |
r = await http_client.get(f"{HUB_URL}/price/{symbol}")
|
| 176 |
+
if r.status_code == 200: return float(r.json().get("mid", 0))
|
| 177 |
+
except: pass
|
|
|
|
|
|
|
| 178 |
bin_symbol = symbol.replace("/", "")
|
| 179 |
try:
|
| 180 |
r = await http_client.get(f"https://fapi.binance.com/fapi/v1/ticker/price?symbol={bin_symbol}")
|
| 181 |
+
if r.status_code == 200: return float(r.json()["price"])
|
| 182 |
+
except: pass
|
|
|
|
|
|
|
| 183 |
return 0.0
|
| 184 |
|
| 185 |
def calculate_max_pain(current_price: float) -> float:
|
|
|
|
| 191 |
bin_sym = "" if native == "XAU" else (native + "USDT")
|
| 192 |
|
| 193 |
tasks = []
|
| 194 |
+
if bin_sym: tasks.extend([fetch_funding_rate(bin_sym), fetch_open_interest(bin_sym), fetch_long_short_ratio(bin_sym)])
|
| 195 |
+
if native == "ETH": tasks.append(fetch_deribit_options("ETH"))
|
| 196 |
+
if native == "XAU": tasks.append(fetch_gold_derivatives())
|
|
|
|
|
|
|
|
|
|
| 197 |
tasks.append(fetch_vix())
|
| 198 |
|
| 199 |
results = await asyncio.gather(*tasks)
|
| 200 |
+
idx = 0; metrics = {}
|
|
|
|
| 201 |
if bin_sym:
|
| 202 |
metrics['funding_rate'] = results[idx]; idx += 1
|
| 203 |
metrics['open_interest'] = results[idx]; idx += 1
|
| 204 |
metrics['long_short_ratio'] = results[idx]; idx += 1
|
| 205 |
+
if native == "ETH": metrics['options'] = results[idx]; idx += 1
|
| 206 |
+
if native == "XAU": metrics['gold_derivatives'] = results[idx]; idx += 1
|
|
|
|
|
|
|
| 207 |
vix_data = results[idx]
|
| 208 |
|
| 209 |
+
signals = []; score = 50.0
|
|
|
|
| 210 |
|
| 211 |
if vix_data["vix"] > 30:
|
| 212 |
signals.append({"source": "VIX", "signal": "BEARISH_RISK", "reason": f"VIX={vix_data['vix']:.1f}"})
|
| 213 |
score += 15 if native == "XAU" else -10
|
| 214 |
+
elif vix_data["vix"] > 25: score += 5
|
|
|
|
| 215 |
|
| 216 |
fund = metrics.get('funding_rate', {})
|
| 217 |
if fund.get('signal') == 'BEARISH':
|
| 218 |
+
signals.append({"source": "FUNDING", "signal": "BEARISH", "reason": f"Перегретый лонг ({fund.get('funding_rate_pct')}%)"}); score -= 15
|
|
|
|
| 219 |
elif fund.get('signal') == 'BULLISH':
|
| 220 |
+
signals.append({"source": "FUNDING", "signal": "BULLISH", "reason": f"Перегретый шорт ({fund.get('funding_rate_pct')}%)"}); score += 15
|
|
|
|
| 221 |
|
| 222 |
oi = metrics.get('open_interest', {})
|
| 223 |
if oi.get('signal') == 'BEARISH':
|
| 224 |
+
signals.append({"source": "OI", "signal": "BEARISH", "reason": f"OI перегрет ({oi.get('change_pct')}%)"}); score -= 10
|
|
|
|
| 225 |
elif oi.get('signal') == 'BULLISH':
|
| 226 |
+
signals.append({"source": "OI", "signal": "BULLISH", "reason": f"OI растёт ({oi.get('change_pct')}%)"}); score += 10
|
|
|
|
| 227 |
|
| 228 |
lsr = metrics.get('long_short_ratio', {})
|
| 229 |
if lsr.get('signal') == 'BEARISH':
|
| 230 |
+
signals.append({"source": "L/S", "signal": "BEARISH", "reason": f"Слишком много лонгов ({lsr.get('long_pct')}%)"}); score -= 10
|
|
|
|
| 231 |
elif lsr.get('signal') == 'BULLISH':
|
| 232 |
+
signals.append({"source": "L/S", "signal": "BULLISH", "reason": f"Много шортов ({lsr.get('short_pct')}%)"}); score += 10
|
|
|
|
| 233 |
|
| 234 |
options = metrics.get('options', {})
|
| 235 |
if options.get('signal') == 'BEARISH':
|
| 236 |
+
signals.append({"source": "OPTIONS", "signal": "BEARISH", "reason": f"PCR={options.get('put_call_ratio_volume')}"}); score -= 8
|
|
|
|
| 237 |
elif options.get('signal') == 'BULLISH':
|
| 238 |
+
signals.append({"source": "OPTIONS", "signal": "BULLISH", "reason": f"PCR={options.get('put_call_ratio_volume')}"}); score += 8
|
|
|
|
| 239 |
|
| 240 |
gold = metrics.get('gold_derivatives', {})
|
| 241 |
if gold.get('signal') == 'BULLISH':
|
| 242 |
+
signals.append({"source": "COT/DXY/TIPS", "signal": "BULLISH", "reason": "Макро-факторы за золото"}); score += 12
|
|
|
|
| 243 |
elif gold.get('signal') == 'BEARISH':
|
| 244 |
+
signals.append({"source": "COT/DXY/TIPS", "signal": "BEARISH", "reason": "Макро-факторы против золота"}); score -= 12
|
|
|
|
| 245 |
|
| 246 |
price = await get_current_price(symbol) if native != "XAU" else 0
|
| 247 |
max_pain = calculate_max_pain(price) if price > 0 else 0
|
| 248 |
if max_pain > 0 and price < max_pain:
|
| 249 |
+
signals.append({"source": "MAX_PAIN", "signal": "BULLISH", "reason": f"Цена ниже Max Pain ({max_pain})"}); score += 8
|
|
|
|
| 250 |
elif max_pain > 0 and price > max_pain:
|
| 251 |
+
signals.append({"source": "MAX_PAIN", "signal": "BEARISH", "reason": f"Цена выше Max Pain ({max_pain})"}); score -= 8
|
|
|
|
| 252 |
|
| 253 |
score = max(0, min(100, score))
|
| 254 |
+
if score > 60: direction, confidence = "LONG", score / 100
|
| 255 |
+
elif score < 40: direction, confidence = "SHORT", (100 - score) / 100
|
| 256 |
+
else: direction, confidence = "WAIT", 0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
|
| 258 |
+
analysis = {"derivative_score": score, "direction": direction, "confidence": round(confidence, 4), "signals": signals, "metrics": {**metrics, "vix": vix_data, "max_pain": max_pain}}
|
| 259 |
OPTIONS_HISTORY.append({"timestamp": datetime.now(timezone.utc).isoformat(), "symbol": symbol, "direction": direction, "score": score})
|
| 260 |
save_history()
|
| 261 |
return analysis
|
|
|
|
| 263 |
# ================= ОТПРАВКА В HUB =================
|
| 264 |
async def send_signal_to_hub(symbol: str, direction: str, confidence: float):
|
| 265 |
try:
|
| 266 |
+
resp = await http_client.post(f"{HUB_URL}/signal", json={
|
| 267 |
+
"space": "space_26_options", "symbol": symbol,
|
| 268 |
+
"direction": direction, "confidence": confidence,
|
|
|
|
|
|
|
| 269 |
"raw": json.dumps({"source": "space_26_options"})
|
| 270 |
+
}, timeout=10)
|
| 271 |
+
if resp.status_code == 200: logger.info(f"📤 {symbol}: {direction} conf={confidence:.3f} отправлен в Hub")
|
| 272 |
+
else: logger.warning(f"Hub вернул {resp.status_code}")
|
| 273 |
+
except Exception as e: logger.error(f"Ошибка отправки в Hub: {e}")
|
| 274 |
|
| 275 |
# ================= ГЛАВНЫЙ СИГНАЛ =================
|
| 276 |
async def get_derivative_signal(symbol: str = "ETH/USD") -> Dict[str, Any]:
|
| 277 |
+
start = time.time(); analysis = await analyze_derivatives(symbol)
|
|
|
|
| 278 |
latency = int((time.time() - start) * 1000)
|
|
|
|
|
|
|
| 279 |
await send_signal_to_hub(symbol, analysis['direction'], analysis['confidence'])
|
| 280 |
+
result = {"space": "space_26_options", "timestamp": int(time.time()), "symbol": symbol, "signal": {"direction": analysis['direction'], "confidence": analysis['confidence']}, "derivative_analysis": analysis, "latency_ms": latency}
|
| 281 |
+
logger.info(f"📊 Options {symbol}: {analysis['direction']} conf={analysis['confidence']:.3f}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
return result
|
| 283 |
|
| 284 |
+
# ================= АВТО-ОТПРАВКА =================
|
| 285 |
+
async def auto_send_loop():
|
| 286 |
+
logger.info(f"🔄 Авто-отправка Options Engine запущена (интервал {AUTO_SEND_INTERVAL}с)")
|
| 287 |
+
await asyncio.sleep(30)
|
| 288 |
+
while True:
|
| 289 |
+
try:
|
| 290 |
+
for symbol in SYMBOLS:
|
| 291 |
+
await get_derivative_signal(symbol); await asyncio.sleep(2)
|
| 292 |
+
logger.info("✅ Options Engine авто-отправка завершена")
|
| 293 |
+
except Exception as e: logger.error(f"Ошибка авто-отправки: {e}")
|
| 294 |
+
await asyncio.sleep(AUTO_SEND_INTERVAL)
|
| 295 |
+
|
| 296 |
# ================= FASTAPI =================
|
| 297 |
+
app = FastAPI(title="Tomiris Space 26 v2.4 — Options & Derivatives Engine (Auto-Hub)")
|
| 298 |
|
| 299 |
@app.on_event("startup")
|
| 300 |
+
async def startup():
|
| 301 |
+
asyncio.create_task(auto_send_loop())
|
| 302 |
+
logger.info("🚀 Space 26 v2.4 запущен с авто-отправкой в Hub")
|
| 303 |
|
| 304 |
@app.on_event("shutdown")
|
| 305 |
async def shutdown(): await http_client.aclose()
|
| 306 |
|
| 307 |
@app.get("/health")
|
| 308 |
async def health():
|
| 309 |
+
return {"status": "operational", "version": "2.4", "hub_url": HUB_URL, "auto_send_interval": AUTO_SEND_INTERVAL, "features": ["VIX", "Funding", "OI", "L/S", "Options PCR", "COT/DXY/TIPS", "Max Pain"]}
|
| 310 |
|
| 311 |
@app.get("/consilium")
|
| 312 |
async def consilium(symbol: str = Query("ETH/USD")):
|
|
|
|
| 315 |
|
| 316 |
@app.get("/funding/{symbol}")
|
| 317 |
async def funding(symbol: str): return await fetch_funding_rate(symbol.upper())
|
|
|
|
| 318 |
@app.get("/oi/{symbol}")
|
| 319 |
async def oi(symbol: str): return await fetch_open_interest(symbol.upper())
|
|
|
|
| 320 |
@app.get("/lsr/{symbol}")
|
| 321 |
async def lsr(symbol: str): return await fetch_long_short_ratio(symbol.upper())
|
|
|
|
| 322 |
@app.get("/options")
|
| 323 |
async def options(): return await fetch_deribit_options("ETH")
|
|
|
|
| 324 |
@app.get("/gold")
|
| 325 |
async def gold(): return await fetch_gold_derivatives()
|
|
|
|
| 326 |
@app.get("/vix")
|
| 327 |
async def vix(): return await fetch_vix()
|
|
|
|
| 328 |
@app.get("/maxpain/{symbol}")
|
| 329 |
async def maxpain(symbol: str):
|
| 330 |
+
price = await get_current_price(symbol); mp = calculate_max_pain(price)
|
|
|
|
| 331 |
return {"symbol": symbol, "current_price": price, "max_pain": mp}
|
|
|
|
| 332 |
@app.get("/history")
|
| 333 |
async def history(limit: int = 50): return list(OPTIONS_HISTORY)[-limit:]
|
| 334 |
+
@app.get("/send_now")
|
| 335 |
+
async def send_now():
|
| 336 |
+
results = {}
|
| 337 |
+
for symbol in SYMBOLS:
|
| 338 |
+
analysis = await get_derivative_signal(symbol)
|
| 339 |
+
results[symbol] = analysis.get("signal", {}).get("direction", "WAIT")
|
| 340 |
+
return {"status": "sent", "results": results}
|
| 341 |
|
| 342 |
if __name__ == "__main__":
|
| 343 |
import uvicorn
|
| 344 |
uvicorn.run(app, host="0.0.0.0", port=7860)
|
| 345 |
|
| 346 |
+
print("🚀 SPACE 26 v2.4 — OPTIONS & DERIVATIVES ENGINE (АВТ��-ОТПРАВКА) ЗАПУЩЕН!")
|