tomirisg25 commited on
Commit
40aadb0
·
verified ·
1 Parent(s): c6924a1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +286 -135
app.py CHANGED
@@ -7,6 +7,7 @@ REQUIRED_PACKAGES = {
7
  'numpy': 'numpy',
8
  'pandas': 'pandas',
9
  'httpx': 'httpx',
 
10
  'fastapi': 'fastapi',
11
  'uvicorn': 'uvicorn',
12
  'requests': 'requests'
@@ -21,14 +22,15 @@ for module_name, pip_name in REQUIRED_PACKAGES.items():
21
  print(f"✅ {pip_name} установлен!")
22
 
23
  # ============================================
24
- # 👑 TOMIRIS SPACE 26 v3.1 — OPTIONS & DERIVATIVES ENGINE (FIXED)
25
  # ============================================
26
  import os, time, json, logging, asyncio
27
- from typing import Dict, Any, List, Optional
28
  from datetime import datetime, timezone
29
  from collections import deque
30
  import numpy as np
31
  import pandas as pd
 
32
  import httpx
33
  from fastapi import FastAPI, Query
34
 
@@ -42,23 +44,48 @@ SYMBOLS = ["XAU/USD", "ETH/USD", "SOL/USD"]
42
 
43
  HUB_URL = os.getenv("HUB_URL", "https://pro-3-tomiris-hub.hf.space")
44
  HUB_SECRET = os.getenv("HUB_SECRET", "TomyrisUltraSecret2026!")
45
- TWELVE_DATA_KEY = os.getenv("TWELVE_DATA_KEY", "a5da25be2e4f41bab4cd78ed8461e180")
46
- FRED_KEY = os.getenv("FRED_KEY", "faa11c8e2e4beee08c5b966e8b63a513")
 
47
 
48
- STARTUP_SLEEP = int(os.getenv("STARTUP_SLEEP", "600"))
49
- AUTO_SEND_INTERVAL = int(os.getenv("AUTO_SEND_INTERVAL", "600"))
50
 
51
- logger.info(f"🔗 Хаб: {HUB_URL}")
52
- logger.info(f"🔑 FRED: {'✅' if FRED_KEY else '❌'} | TwelveData: {'✅' if TWELVE_DATA_KEY else '❌'}")
 
 
 
53
 
54
  HISTORY_FILE = "options_history.json"
55
- CACHE_TTL = {"funding": 60, "oi": 60, "lsr": 120, "deribit": 300, "fred": 3600, "vix": 60, "maxpain": 300}
56
 
57
  http_client = httpx.AsyncClient(timeout=15.0)
58
 
59
  def hub_headers():
60
  return {"X-Hub-Secret": HUB_SECRET, "Content-Type": "application/json"}
61
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  cache_store = {}
63
  cache_times = {}
64
  CIRCUIT_BREAKER = {}
@@ -92,12 +119,13 @@ def save_history():
92
  # ================= ЗАГРУЗКА ДАННЫХ =================
93
  async def fetch_vix() -> Dict[str, Any]:
94
  if breaker_open("vix"): return {"vix": 20.0, "level": "NORMAL", "signal": "NEUTRAL"}
 
95
  try:
96
  r = await http_client.get(f"https://api.twelvedata.com/quote?symbol=VIX&apikey={TWELVE_DATA_KEY}")
97
  if r.status_code == 200:
98
  data = r.json(); vix_val = float(data.get("close", 20))
99
- level = "HIGH" if vix_val > 30 else "ELEVATED" if vix_val > 25 else "NORMAL"
100
- signal = "BEARISH_RISK" if vix_val > 30 else "NEUTRAL"
101
  breaker_record("vix", True)
102
  return {"vix": vix_val, "level": level, "signal": signal}
103
  except: breaker_record("vix", False)
@@ -106,14 +134,30 @@ async def fetch_vix() -> Dict[str, Any]:
106
  async def fetch_funding_rate(symbol: str) -> Dict[str, Any]:
107
  if breaker_open(f"funding_{symbol}"): return {"funding_rate": 0, "signal": "NEUTRAL"}
108
  try:
109
- r = await http_client.get("https://fapi.binance.com/fapi/v1/premiumIndex")
110
  if r.status_code == 200:
111
- for item in r.json():
112
- if item.get('symbol') == symbol:
113
- fr = float(item.get('lastFundingRate', 0))
114
- signal = "BEARISH" if fr > 0.001 else "BULLISH" if fr < -0.001 else "NEUTRAL"
115
- breaker_record(f"funding_{symbol}", True)
116
- return {"funding_rate": fr, "funding_rate_pct": round(fr * 100, 4), "signal": signal}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  except: breaker_record(f"funding_{symbol}", False)
118
  return {"funding_rate": 0, "signal": "NEUTRAL"}
119
 
@@ -126,9 +170,18 @@ async def fetch_open_interest(symbol: str) -> Dict[str, Any]:
126
  prev_key = f"oi_{symbol}_prev"; prev_oi = cache_store.get(prev_key, oi)
127
  change = ((oi - prev_oi) / prev_oi * 100) if prev_oi > 0 else 0
128
  cache_store[prev_key] = oi
129
- signal = "BULLISH" if change > 3 else "BEARISH" if change < -3 else "NEUTRAL"
 
 
 
 
 
 
 
 
 
130
  breaker_record(f"oi_{symbol}", True)
131
- return {"open_interest": oi, "change_pct": round(change, 2), "signal": signal}
132
  except: breaker_record(f"oi_{symbol}", False)
133
  return {"open_interest": 0, "change_pct": 0, "signal": "NEUTRAL"}
134
 
@@ -139,7 +192,9 @@ async def fetch_long_short_ratio(symbol: str) -> Dict[str, Any]:
139
  if r.status_code == 200:
140
  lsr = float(r.json().get("longShortRatio", 1))
141
  long_pct = lsr / (1 + lsr) * 100; short_pct = 100 - long_pct
142
- if lsr > 2.0: signal = "BEARISH"
 
 
143
  elif lsr < 0.5: signal = "BULLISH"
144
  else: signal = "NEUTRAL"
145
  breaker_record(f"lsr_{symbol}", True)
@@ -155,46 +210,76 @@ async def fetch_deribit_options(coin: str = "ETH") -> Dict[str, Any]:
155
  instruments = r.json().get('result', [])
156
  call_vol = sum(float(i.get('volume', 0)) for i in instruments if i.get('option_type') == 'call')
157
  put_vol = sum(float(i.get('volume', 0)) for i in instruments if i.get('option_type') == 'put')
158
- pcr = put_vol / call_vol if call_vol > 0 else 1.0
159
- signal = "BEARISH" if pcr > 1.3 else "BULLISH" if pcr < 0.7 else "NEUTRAL"
 
 
 
 
 
 
 
 
 
 
 
 
160
  breaker_record("deribit", True)
161
- return {"put_call_ratio_volume": round(pcr, 4), "signal": signal}
 
 
 
 
162
  except: breaker_record("deribit", False)
163
- return {"put_call_ratio_volume": 1.0, "signal": "NEUTRAL"}
164
 
165
  async def fetch_gold_derivatives() -> Dict[str, Any]:
166
  if breaker_open("fred"): return {"signal": "NEUTRAL"}
167
- try:
168
- dxy_r = await http_client.get(f"https://api.stlouisfed.org/fred/series/observations?series_id=DTWEXBGS&api_key={FRED_KEY}&file_type=json&limit=30&sort_order=desc")
169
- tips_r = await http_client.get(f"https://api.stlouisfed.org/fred/series/observations?series_id=DFII10&api_key={FRED_KEY}&file_type=json&limit=30&sort_order=desc")
170
- dxy_vals = [float(o['value']) for o in dxy_r.json().get('observations', []) if o['value'] != '.'] if dxy_r.status_code == 200 else []
171
- tips_vals = [float(o['value']) for o in tips_r.json().get('observations', []) if o['value'] != '.'] if tips_r.status_code == 200 else []
172
- dxy_change = ((dxy_vals[0] - dxy_vals[-1]) / dxy_vals[-1] * 100) if len(dxy_vals) >= 2 else 0
173
- tips_current = tips_vals[0] if tips_vals else 0.5
174
- signal = "BULLISH" if dxy_change < -2 and tips_current < 0 else "BEARISH" if dxy_change > 2 and tips_current > 0 else "NEUTRAL"
175
- breaker_record("fred", True)
176
- return {"dxy": dxy_vals[0] if dxy_vals else 104.5, "dxy_change_pct": round(dxy_change, 2), "tips_yield": tips_current, "signal": signal}
177
- except: breaker_record("fred", False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  return {"signal": "NEUTRAL"}
179
 
180
  async def get_current_price(symbol: str) -> float:
181
  try:
182
  r = await http_client.get(f"{HUB_URL}/price/{symbol}", headers=hub_headers())
183
- if r.status_code == 200:
184
  data = r.json()
185
  return float(data.get("price", data.get("mid", 0)))
186
  except: pass
187
- bin_symbol = symbol.replace("/", "")
188
- try:
189
- r = await http_client.get(f"https://fapi.binance.com/fapi/v1/ticker/price?symbol={bin_symbol}")
190
- if r.status_code == 200: return float(r.json()["price"])
191
- except: pass
192
  return 0.0
193
 
194
  def calculate_max_pain(current_price: float) -> float:
195
  return round(current_price * 0.98, 2) if current_price > 0 else 0.0
196
 
197
- # ================= АНАЛИЗ =================
198
  async def analyze_derivatives(symbol: str) -> Dict[str, Any]:
199
  native = "XAU" if "XAU" in symbol else ("ETH" if "ETH" in symbol else "SOL")
200
  bin_sym = "" if native == "XAU" else (native + "USDT")
@@ -207,6 +292,7 @@ async def analyze_derivatives(symbol: str) -> Dict[str, Any]:
207
 
208
  results = await asyncio.gather(*tasks)
209
  idx = 0; metrics = {}
 
210
  if bin_sym:
211
  metrics['funding_rate'] = results[idx]; idx += 1
212
  metrics['open_interest'] = results[idx]; idx += 1
@@ -215,103 +301,154 @@ async def analyze_derivatives(symbol: str) -> Dict[str, Any]:
215
  if native == "XAU": metrics['gold_derivatives'] = results[idx]; idx += 1
216
  vix_data = results[idx]
217
 
218
- signals = []; score = 50.0
219
-
220
- if vix_data["vix"] > 30:
221
- signals.append({"source": "VIX", "signal": "BEARISH_RISK", "reason": f"VIX={vix_data['vix']:.1f}"})
222
- score += 15 if native == "XAU" else -10
223
- elif vix_data["vix"] > 25: score += 5
 
 
 
 
 
 
 
 
 
 
224
 
 
225
  fund = metrics.get('funding_rate', {})
226
- if fund.get('signal') == 'BEARISH':
227
- signals.append({"source": "FUNDING", "signal": "BEARISH", "reason": f"Перегретый лонг ({fund.get('funding_rate_pct')}%)"}); score -= 15
228
- elif fund.get('signal') == 'BULLISH':
229
- signals.append({"source": "FUNDING", "signal": "BULLISH", "reason": f"Перегретый шорт ({fund.get('funding_rate_pct')}%)"}); score += 15
 
 
 
 
 
 
 
 
 
 
 
230
 
 
231
  oi = metrics.get('open_interest', {})
232
- if oi.get('signal') == 'BEARISH':
233
- signals.append({"source": "OI", "signal": "BEARISH", "reason": f"OI перегрет ({oi.get('change_pct')}%)"}); score -= 10
234
- elif oi.get('signal') == 'BULLISH':
235
- signals.append({"source": "OI", "signal": "BULLISH", "reason": f"OI растёт ({oi.get('change_pct')}%)"}); score += 10
 
 
 
 
 
 
 
236
 
 
237
  lsr = metrics.get('long_short_ratio', {})
238
- if lsr.get('signal') == 'BEARISH':
239
- signals.append({"source": "L/S", "signal": "BEARISH", "reason": f"Слишком много лонгов ({lsr.get('long_pct')}%)"}); score -= 10
240
- elif lsr.get('signal') == 'BULLISH':
241
- signals.append({"source": "L/S", "signal": "BULLISH", "reason": f"Много шортов ({lsr.get('short_pct')}%)"}); score += 10
 
 
 
 
 
 
 
242
 
 
243
  options = metrics.get('options', {})
244
- if options.get('signal') == 'BEARISH':
245
- signals.append({"source": "OPTIONS", "signal": "BEARISH", "reason": f"PCR={options.get('put_call_ratio_volume')}"}); score -= 8
 
 
 
 
246
  elif options.get('signal') == 'BULLISH':
247
- signals.append({"source": "OPTIONS", "signal": "BULLISH", "reason": f"PCR={options.get('put_call_ratio_volume')}"}); score += 8
 
 
248
 
 
249
  gold = metrics.get('gold_derivatives', {})
250
- if gold.get('signal') == 'BULLISH':
251
- signals.append({"source": "COT/DXY/TIPS", "signal": "BULLISH", "reason": "Макро-факторы за золото"}); score += 12
 
 
 
 
252
  elif gold.get('signal') == 'BEARISH':
253
- signals.append({"source": "COT/DXY/TIPS", "signal": "BEARISH", "reason": "Макро-факторы против золота"}); score -= 12
 
 
254
 
 
255
  price = await get_current_price(symbol) if native != "XAU" else 0
256
  max_pain = calculate_max_pain(price) if price > 0 else 0
257
- if max_pain > 0 and price < max_pain:
258
- signals.append({"source": "MAX_PAIN", "signal": "BULLISH", "reason": f"Цена ниже Max Pain ({max_pain})"}); score += 8
259
- elif max_pain > 0 and price > max_pain:
260
- signals.append({"source": "MAX_PAIN", "signal": "BEARISH", "reason": f"Цена выше Max Pain ({max_pain})"}); score -= 8
261
-
262
- score = max(0, min(100, score))
263
-
264
- # 🔥 BUY/SELL вместо LONG/SHORT
265
- if score > 60:
266
- signal = "BUY"
267
- confidence = score / 100
268
- elif score < 40:
269
- signal = "SELL"
270
- confidence = (100 - score) / 100
271
  else:
272
- signal = "WAIT"
273
- confidence = 0.0
 
 
 
 
 
 
 
 
 
 
 
274
 
275
- analysis = {"derivative_score": score, "signal": signal, "confidence": round(confidence, 4), "signals": signals, "metrics": {**metrics, "vix": vix_data, "max_pain": max_pain}}
276
- OPTIONS_HISTORY.append({"timestamp": datetime.now(timezone.utc).isoformat(), "symbol": symbol, "signal": signal, "score": score})
 
 
 
 
 
 
 
 
 
 
 
277
  save_history()
 
278
  return analysis
279
 
280
- # ================= ОТПРАВКА В HUB (ИСПРАВЛЕНО) =================
281
  async def send_signal_to_hub(symbol: str, signal: str, confidence: float, features: Dict = None):
282
- if features is None:
283
- features = {}
284
-
285
  payload = {
286
- "space_id": SPACE_ID,
287
- "space_name": SPACE_NAME,
288
- "symbol": symbol,
289
- "signal": signal,
290
- "confidence": round(confidence, 4),
291
- "features": features,
292
- "metadata": {"version": "3.1"},
293
- "timestamp": datetime.now().isoformat()
294
  }
295
-
296
  for attempt in range(3):
297
  try:
298
- headers = hub_headers()
299
- r = await http_client.post(f"{HUB_URL}/signals", json=payload, timeout=15, headers=headers)
300
  if r.status_code == 200:
301
  logger.info(f"📤 {symbol}: {signal} conf={confidence:.3f}")
302
- return
303
- elif r.status_code == 429:
304
- wait = 3 * (attempt + 1)
305
- logger.warning(f"⏳ 429 для {symbol}, жду {wait}с...")
306
- await asyncio.sleep(wait)
307
- else:
308
- logger.warning(f"Попытка {attempt+1}: HTTP {r.status_code}")
309
- await asyncio.sleep(2)
310
  except Exception as e:
311
  logger.warning(f"Попытка {attempt+1}: {e}")
312
  await asyncio.sleep(2)
313
-
314
- logger.error(f"❌ Не удалось отправить {symbol}")
315
 
316
  # ================= ГЛАВНЫЙ СИГНАЛ =================
317
  async def get_derivative_signal(symbol: str = "ETH/USD") -> Dict[str, Any]:
@@ -319,24 +456,25 @@ async def get_derivative_signal(symbol: str = "ETH/USD") -> Dict[str, Any]:
319
  analysis = await analyze_derivatives(symbol)
320
  latency = int((time.time() - start) * 1000)
321
 
322
- features = {"derivative_score": analysis['derivative_score']}
 
 
 
323
  await send_signal_to_hub(symbol, analysis['signal'], analysis['confidence'], features)
324
 
325
- logger.info(f"📊 Options {symbol}: {analysis['signal']} conf={analysis['confidence']:.3f} | {latency}ms")
326
  return {
327
- "space_id": SPACE_ID,
328
- "timestamp": int(time.time()),
329
- "symbol": symbol,
330
- "signal": analysis['signal'],
331
- "confidence": analysis['confidence'],
332
- "derivative_analysis": analysis
333
  }
334
 
335
  # ================= АВТО-ОТПРАВКА =================
336
  async def auto_send_loop():
337
  logger.info(f"⏳ Стартовый сон {STARTUP_SLEEP}с...")
 
338
  await asyncio.sleep(STARTUP_SLEEP)
339
- logger.info(f"🔄 Options Engine запущен (интервал {AUTO_SEND_INTERVAL}с)")
340
  while True:
341
  try:
342
  for symbol in SYMBOLS:
@@ -344,28 +482,25 @@ async def auto_send_loop():
344
  await asyncio.sleep(2)
345
  logger.info("✅ Options Engine цикл завершён")
346
  except Exception as e:
347
- logger.error(f"Ошибка авто-отправки: {e}")
 
348
  await asyncio.sleep(AUTO_SEND_INTERVAL)
349
 
350
  # ================= FASTAPI =================
351
- app = FastAPI(title="Tomiris Space 26 v3.1 — Options & Derivatives")
352
 
353
  @app.on_event("startup")
354
  async def startup():
355
  asyncio.create_task(auto_send_loop())
356
- logger.info(f"🚀 Space 26 v3.1 ЗАПУЩЕН | Хаб: {HUB_URL}")
357
 
358
  @app.on_event("shutdown")
359
- async def shutdown():
360
- await http_client.aclose()
361
 
362
  @app.get("/health")
363
- async def health():
364
- return {"space_id": SPACE_ID, "status": "operational", "version": "3.1"}
365
-
366
  @app.head("/health")
367
- async def health_head():
368
- return {}
369
 
370
  @app.get("/consilium")
371
  async def consilium(symbol: str = Query("ETH/USD")):
@@ -373,23 +508,40 @@ async def consilium(symbol: str = Query("ETH/USD")):
373
  return await get_derivative_signal(symbol)
374
 
375
  @app.get("/funding/{symbol}")
376
- async def funding(symbol: str): return await fetch_funding_rate(symbol.upper())
 
 
 
 
377
  @app.get("/oi/{symbol}")
378
- async def oi(symbol: str): return await fetch_open_interest(symbol.upper())
 
 
 
 
379
  @app.get("/lsr/{symbol}")
380
- async def lsr(symbol: str): return await fetch_long_short_ratio(symbol.upper())
 
 
 
 
381
  @app.get("/options")
382
  async def options(): return await fetch_deribit_options("ETH")
 
383
  @app.get("/gold")
384
  async def gold(): return await fetch_gold_derivatives()
 
385
  @app.get("/vix")
386
  async def vix(): return await fetch_vix()
 
387
  @app.get("/maxpain/{symbol}")
388
  async def maxpain(symbol: str):
389
  price = await get_current_price(symbol); mp = calculate_max_pain(price)
390
  return {"symbol": symbol, "current_price": price, "max_pain": mp}
 
391
  @app.get("/history")
392
  async def history(limit: int = 50): return list(OPTIONS_HISTORY)[-limit:]
 
393
  @app.get("/send_now")
394
  async def send_now():
395
  results = {}
@@ -399,11 +551,10 @@ async def send_now():
399
  return {"status": "sent", "results": results}
400
 
401
  @app.get("/")
402
- async def root():
403
- return {"name": "Options Engine v3.1", "space_id": SPACE_ID, "hub": HUB_URL}
404
 
405
  if __name__ == "__main__":
406
  import uvicorn
407
  uvicorn.run(app, host="0.0.0.0", port=7860)
408
 
409
- print("🚀 SPACE 26 v3.1 — OPTIONS & DERIVATIVES ENGINE ГОТОВ К РАБОТЕ!")
 
7
  'numpy': 'numpy',
8
  'pandas': 'pandas',
9
  'httpx': 'httpx',
10
+ 'scipy': 'scipy',
11
  'fastapi': 'fastapi',
12
  'uvicorn': 'uvicorn',
13
  'requests': 'requests'
 
22
  print(f"✅ {pip_name} установлен!")
23
 
24
  # ============================================
25
+ # 👑 TOMIRIS SPACE 26 v4.0 — OPTIONS & DERIVATIVES ENGINE (УСИЛЕННЫЙ)
26
  # ============================================
27
  import os, time, json, logging, asyncio
28
+ from typing import Dict, Any, List, Optional, Tuple
29
  from datetime import datetime, timezone
30
  from collections import deque
31
  import numpy as np
32
  import pandas as pd
33
+ from scipy import stats
34
  import httpx
35
  from fastapi import FastAPI, Query
36
 
 
44
 
45
  HUB_URL = os.getenv("HUB_URL", "https://pro-3-tomiris-hub.hf.space")
46
  HUB_SECRET = os.getenv("HUB_SECRET", "TomyrisUltraSecret2026!")
47
+ TWELVE_DATA_KEY = os.getenv("TWELVE_DATA_KEY", "")
48
+ FRED_KEY = os.getenv("FRED_KEY", "")
49
+ FRED_KEY_2 = os.getenv("FRED_KEY_2", "")
50
 
51
+ FRED_KEYS = [k for k in [FRED_KEY, FRED_KEY_2] if k]
52
+ if not FRED_KEYS: FRED_KEYS = ["no_key"]
53
 
54
+ STARTUP_SLEEP = int(os.getenv("STARTUP_SLEEP", "120"))
55
+ AUTO_SEND_INTERVAL = int(os.getenv("AUTO_SEND_INTERVAL", "300"))
56
+
57
+ logger.info(f"🔗 Хаб: {HUB_URL} | Старт: {STARTUP_SLEEP}с | Интервал: {AUTO_SEND_INTERVAL}с")
58
+ logger.info(f"🔑 FRED: {len(FRED_KEYS)} | TwelveData: {'✓' if TWELVE_DATA_KEY else '✗'}")
59
 
60
  HISTORY_FILE = "options_history.json"
61
+ CACHE_TTL = {"funding": 60, "oi": 60, "lsr": 120, "deribit": 300, "fred": 3600, "vix": 60}
62
 
63
  http_client = httpx.AsyncClient(timeout=15.0)
64
 
65
  def hub_headers():
66
  return {"X-Hub-Secret": HUB_SECRET, "Content-Type": "application/json"}
67
 
68
+ async def log_to_hub(event_type: str, message: str, details: dict = None):
69
+ try:
70
+ await http_client.post(
71
+ f"{HUB_URL}/log",
72
+ json={"space_id": str(SPACE_ID), "event_type": event_type, "message": message, "details": details or {}},
73
+ headers=hub_headers(), timeout=5
74
+ )
75
+ except: pass
76
+
77
+ # ================= ИСТОРИЯ ДЛЯ Z-SCORE =================
78
+ SCORE_HISTORY = {sym: deque(maxlen=200) for sym in SYMBOLS}
79
+ FUNDING_HISTORY = {sym: deque(maxlen=200) for sym in SYMBOLS}
80
+ OI_HISTORY = {sym: deque(maxlen=200) for sym in SYMBOLS}
81
+
82
+ def calculate_zscore(current: float, history: deque) -> float:
83
+ if len(history) < 10: return 0.0
84
+ arr = np.array(list(history))
85
+ mean, std = arr.mean(), arr.std()
86
+ if std == 0: return 0.0
87
+ return (current - mean) / std
88
+
89
  cache_store = {}
90
  cache_times = {}
91
  CIRCUIT_BREAKER = {}
 
119
  # ================= ЗАГРУЗКА ДАННЫХ =================
120
  async def fetch_vix() -> Dict[str, Any]:
121
  if breaker_open("vix"): return {"vix": 20.0, "level": "NORMAL", "signal": "NEUTRAL"}
122
+ if not TWELVE_DATA_KEY: return {"vix": 20.0, "level": "NORMAL", "signal": "NEUTRAL"}
123
  try:
124
  r = await http_client.get(f"https://api.twelvedata.com/quote?symbol=VIX&apikey={TWELVE_DATA_KEY}")
125
  if r.status_code == 200:
126
  data = r.json(); vix_val = float(data.get("close", 20))
127
+ level = "CRITICAL" if vix_val > 35 else "HIGH" if vix_val > 28 else "ELEVATED" if vix_val > 22 else "NORMAL"
128
+ signal = "STRONG_SELL" if vix_val > 35 else "SELL" if vix_val > 28 else "CAUTION" if vix_val > 22 else "NEUTRAL"
129
  breaker_record("vix", True)
130
  return {"vix": vix_val, "level": level, "signal": signal}
131
  except: breaker_record("vix", False)
 
134
  async def fetch_funding_rate(symbol: str) -> Dict[str, Any]:
135
  if breaker_open(f"funding_{symbol}"): return {"funding_rate": 0, "signal": "NEUTRAL"}
136
  try:
137
+ r = await http_client.get(f"https://fapi.binance.com/fapi/v1/premiumIndex?symbol={symbol}")
138
  if r.status_code == 200:
139
+ data = r.json()
140
+ if isinstance(data, list):
141
+ for item in data:
142
+ if item.get('symbol') == symbol:
143
+ fr = float(item.get('lastFundingRate', 0))
144
+ break
145
+ elif isinstance(data, dict):
146
+ fr = float(data.get('lastFundingRate', 0))
147
+ else:
148
+ return {"funding_rate": 0, "signal": "NEUTRAL"}
149
+
150
+ FUNDING_HISTORY[symbol].append(fr)
151
+ fr_z = calculate_zscore(fr, FUNDING_HISTORY[symbol])
152
+
153
+ if fr > 0.005: signal = "STRONG_BEARISH"
154
+ elif fr > 0.001: signal = "BEARISH"
155
+ elif fr < -0.005: signal = "STRONG_BULLISH"
156
+ elif fr < -0.001: signal = "BULLISH"
157
+ else: signal = "NEUTRAL"
158
+
159
+ breaker_record(f"funding_{symbol}", True)
160
+ return {"funding_rate": fr, "funding_rate_pct": round(fr * 100, 4), "zscore": round(fr_z, 2), "signal": signal}
161
  except: breaker_record(f"funding_{symbol}", False)
162
  return {"funding_rate": 0, "signal": "NEUTRAL"}
163
 
 
170
  prev_key = f"oi_{symbol}_prev"; prev_oi = cache_store.get(prev_key, oi)
171
  change = ((oi - prev_oi) / prev_oi * 100) if prev_oi > 0 else 0
172
  cache_store[prev_key] = oi
173
+
174
+ OI_HISTORY[symbol].append(change)
175
+ oi_z = calculate_zscore(change, OI_HISTORY[symbol])
176
+
177
+ if change > 10: signal = "STRONG_BULLISH"
178
+ elif change > 3: signal = "BULLISH"
179
+ elif change < -10: signal = "STRONG_BEARISH"
180
+ elif change < -3: signal = "BEARISH"
181
+ else: signal = "NEUTRAL"
182
+
183
  breaker_record(f"oi_{symbol}", True)
184
+ return {"open_interest": oi, "change_pct": round(change, 2), "zscore": round(oi_z, 2), "signal": signal}
185
  except: breaker_record(f"oi_{symbol}", False)
186
  return {"open_interest": 0, "change_pct": 0, "signal": "NEUTRAL"}
187
 
 
192
  if r.status_code == 200:
193
  lsr = float(r.json().get("longShortRatio", 1))
194
  long_pct = lsr / (1 + lsr) * 100; short_pct = 100 - long_pct
195
+ if lsr > 3.0: signal = "STRONG_BEARISH"
196
+ elif lsr > 2.0: signal = "BEARISH"
197
+ elif lsr < 0.33: signal = "STRONG_BULLISH"
198
  elif lsr < 0.5: signal = "BULLISH"
199
  else: signal = "NEUTRAL"
200
  breaker_record(f"lsr_{symbol}", True)
 
210
  instruments = r.json().get('result', [])
211
  call_vol = sum(float(i.get('volume', 0)) for i in instruments if i.get('option_type') == 'call')
212
  put_vol = sum(float(i.get('volume', 0)) for i in instruments if i.get('option_type') == 'put')
213
+
214
+ # Open Interest тоже
215
+ call_oi = sum(float(i.get('open_interest', 0)) for i in instruments if i.get('option_type') == 'call')
216
+ put_oi = sum(float(i.get('open_interest', 0)) for i in instruments if i.get('option_type') == 'put')
217
+
218
+ pcr_volume = put_vol / call_vol if call_vol > 0 else 1.0
219
+ pcr_oi = put_oi / call_oi if call_oi > 0 else 1.0
220
+
221
+ if pcr_volume > 1.5: signal = "STRONG_BEARISH"
222
+ elif pcr_volume > 1.2: signal = "BEARISH"
223
+ elif pcr_volume < 0.6: signal = "STRONG_BULLISH"
224
+ elif pcr_volume < 0.8: signal = "BULLISH"
225
+ else: signal = "NEUTRAL"
226
+
227
  breaker_record("deribit", True)
228
+ return {
229
+ "put_call_ratio_volume": round(pcr_volume, 4),
230
+ "put_call_ratio_oi": round(pcr_oi, 4),
231
+ "signal": signal
232
+ }
233
  except: breaker_record("deribit", False)
234
+ return {"put_call_ratio_volume": 1.0, "put_call_ratio_oi": 1.0, "signal": "NEUTRAL"}
235
 
236
  async def fetch_gold_derivatives() -> Dict[str, Any]:
237
  if breaker_open("fred"): return {"signal": "NEUTRAL"}
238
+ if not FRED_KEYS or FRED_KEYS == ["no_key"]: return {"signal": "NEUTRAL"}
239
+
240
+ for key in FRED_KEYS:
241
+ try:
242
+ dxy_r = await http_client.get(f"https://api.stlouisfed.org/fred/series/observations?series_id=DTWEXBGS&api_key={key}&file_type=json&limit=30&sort_order=desc")
243
+ tips_r = await http_client.get(f"https://api.stlouisfed.org/fred/series/observations?series_id=DFII10&api_key={key}&file_type=json&limit=30&sort_order=desc")
244
+
245
+ if dxy_r.status_code == 200 and tips_r.status_code == 200:
246
+ dxy_vals = [float(o['value']) for o in dxy_r.json().get('observations', []) if o['value'] != '.']
247
+ tips_vals = [float(o['value']) for o in tips_r.json().get('observations', []) if o['value'] != '.']
248
+
249
+ dxy_change = ((dxy_vals[0] - dxy_vals[-1]) / dxy_vals[-1] * 100) if len(dxy_vals) >= 2 else 0
250
+ tips_current = tips_vals[0] if tips_vals else 0.5
251
+
252
+ if dxy_change < -2 and tips_current < 0: signal = "STRONG_BULLISH"
253
+ elif dxy_change < 0 and tips_current < 0: signal = "BULLISH"
254
+ elif dxy_change > 2 and tips_current > 0: signal = "STRONG_BEARISH"
255
+ elif dxy_change > 0 and tips_current > 0: signal = "BEARISH"
256
+ else: signal = "NEUTRAL"
257
+
258
+ breaker_record("fred", True)
259
+ return {
260
+ "dxy": dxy_vals[0] if dxy_vals else 104.5,
261
+ "dxy_change_pct": round(dxy_change, 2),
262
+ "tips_yield": tips_current,
263
+ "signal": signal
264
+ }
265
+ except: continue
266
+
267
+ breaker_record("fred", False)
268
  return {"signal": "NEUTRAL"}
269
 
270
  async def get_current_price(symbol: str) -> float:
271
  try:
272
  r = await http_client.get(f"{HUB_URL}/price/{symbol}", headers=hub_headers())
273
+ if r.status_code == 200:
274
  data = r.json()
275
  return float(data.get("price", data.get("mid", 0)))
276
  except: pass
 
 
 
 
 
277
  return 0.0
278
 
279
  def calculate_max_pain(current_price: float) -> float:
280
  return round(current_price * 0.98, 2) if current_price > 0 else 0.0
281
 
282
+ # ================= 🔥 АНАЛИЗ =================
283
  async def analyze_derivatives(symbol: str) -> Dict[str, Any]:
284
  native = "XAU" if "XAU" in symbol else ("ETH" if "ETH" in symbol else "SOL")
285
  bin_sym = "" if native == "XAU" else (native + "USDT")
 
292
 
293
  results = await asyncio.gather(*tasks)
294
  idx = 0; metrics = {}
295
+
296
  if bin_sym:
297
  metrics['funding_rate'] = results[idx]; idx += 1
298
  metrics['open_interest'] = results[idx]; idx += 1
 
301
  if native == "XAU": metrics['gold_derivatives'] = results[idx]; idx += 1
302
  vix_data = results[idx]
303
 
304
+ signals = {}
305
+ score = 50.0
306
+
307
+ # VIX (вес 15%)
308
+ vix_val = vix_data.get("vix", 20)
309
+ if vix_val > 35:
310
+ signals["vix"] = ("STRONG_SELL", 18) if native != "XAU" else ("STRONG_BUY", 18)
311
+ score += 18 if native == "XAU" else -18
312
+ elif vix_val > 28:
313
+ signals["vix"] = ("SELL", 12) if native != "XAU" else ("BUY", 12)
314
+ score += 12 if native == "XAU" else -12
315
+ elif vix_val > 22:
316
+ score += 6 if native == "XAU" else -6
317
+ signals["vix"] = ("SLIGHT_BUY", 6) if native == "XAU" else ("SLIGHT_SELL", 6)
318
+ else:
319
+ signals["vix"] = ("NEUTRAL", 0)
320
 
321
+ # Funding Rate (вес 20%)
322
  fund = metrics.get('funding_rate', {})
323
+ fund_signal = fund.get('signal', 'NEUTRAL')
324
+ fund_z = fund.get('zscore', 0)
325
+ if fund_signal == 'STRONG_BEARISH':
326
+ score -= 20; signals["funding"] = ("STRONG_SELL", 20)
327
+ elif fund_signal == 'BEARISH':
328
+ score -= 14; signals["funding"] = ("SELL", 14)
329
+ elif fund_signal == 'STRONG_BULLISH':
330
+ score += 20; signals["funding"] = ("STRONG_BUY", 20)
331
+ elif fund_signal == 'BULLISH':
332
+ score += 14; signals["funding"] = ("BUY", 14)
333
+ else:
334
+ signals["funding"] = ("NEUTRAL", 0)
335
+
336
+ if abs(fund_z) > 2.0:
337
+ score += 8 if fund_z < 0 else -8
338
 
339
+ # Open Interest (вес 15%)
340
  oi = metrics.get('open_interest', {})
341
+ oi_signal = oi.get('signal', 'NEUTRAL')
342
+ if oi_signal == 'STRONG_BULLISH':
343
+ score += 12; signals["oi"] = ("BUY", 12)
344
+ elif oi_signal == 'BULLISH':
345
+ score += 8; signals["oi"] = ("SLIGHT_BUY", 8)
346
+ elif oi_signal == 'STRONG_BEARISH':
347
+ score -= 12; signals["oi"] = ("SELL", 12)
348
+ elif oi_signal == 'BEARISH':
349
+ score -= 8; signals["oi"] = ("SLIGHT_SELL", 8)
350
+ else:
351
+ signals["oi"] = ("NEUTRAL", 0)
352
 
353
+ # Long/Short Ratio (вес 15%)
354
  lsr = metrics.get('long_short_ratio', {})
355
+ lsr_signal = lsr.get('signal', 'NEUTRAL')
356
+ if lsr_signal == 'STRONG_BEARISH':
357
+ score -= 15; signals["lsr"] = ("STRONG_SELL", 15)
358
+ elif lsr_signal == 'BEARISH':
359
+ score -= 10; signals["lsr"] = ("SELL", 10)
360
+ elif lsr_signal == 'STRONG_BULLISH':
361
+ score += 15; signals["lsr"] = ("STRONG_BUY", 15)
362
+ elif lsr_signal == 'BULLISH':
363
+ score += 10; signals["lsr"] = ("BUY", 10)
364
+ else:
365
+ signals["lsr"] = ("NEUTRAL", 0)
366
 
367
+ # Options PCR (вес 15%)
368
  options = metrics.get('options', {})
369
+ if options.get('signal') == 'STRONG_BEARISH':
370
+ score -= 12; signals["options"] = ("SELL", 12)
371
+ elif options.get('signal') == 'BEARISH':
372
+ score -= 8; signals["options"] = ("SLIGHT_SELL", 8)
373
+ elif options.get('signal') == 'STRONG_BULLISH':
374
+ score += 12; signals["options"] = ("BUY", 12)
375
  elif options.get('signal') == 'BULLISH':
376
+ score += 8; signals["options"] = ("SLIGHT_BUY", 8)
377
+ else:
378
+ signals["options"] = ("NEUTRAL", 0)
379
 
380
+ # Gold Derivatives (вес 10%)
381
  gold = metrics.get('gold_derivatives', {})
382
+ if gold.get('signal') == 'STRONG_BULLISH':
383
+ score += 12; signals["gold"] = ("STRONG_BUY", 12)
384
+ elif gold.get('signal') == 'BULLISH':
385
+ score += 8; signals["gold"] = ("BUY", 8)
386
+ elif gold.get('signal') == 'STRONG_BEARISH':
387
+ score -= 12; signals["gold"] = ("STRONG_SELL", 12)
388
  elif gold.get('signal') == 'BEARISH':
389
+ score -= 8; signals["gold"] = ("SELL", 8)
390
+ else:
391
+ signals["gold"] = ("NEUTRAL", 0)
392
 
393
+ # Max Pain (вес 10%)
394
  price = await get_current_price(symbol) if native != "XAU" else 0
395
  max_pain = calculate_max_pain(price) if price > 0 else 0
396
+ if max_pain > 0 and price < max_pain * 0.97:
397
+ score += 8; signals["maxpain"] = ("BUY", 8)
398
+ elif max_pain > 0 and price > max_pain * 1.03:
399
+ score -= 8; signals["maxpain"] = ("SELL", 8)
 
 
 
 
 
 
 
 
 
 
400
  else:
401
+ signals["maxpain"] = ("NEUTRAL", 0)
402
+
403
+ score = round(max(3, min(97, score)), 1)
404
+
405
+ # Z-score
406
+ SCORE_HISTORY[symbol].append(score)
407
+ score_z = calculate_zscore(score, SCORE_HISTORY[symbol])
408
+
409
+ if score > 62: signal, confidence = "BUY", min(0.92, score / 100)
410
+ elif score > 54: signal, confidence = "BUY", min(0.68, (score - 50) / 50)
411
+ elif score < 38: signal, confidence = "SELL", min(0.92, (100 - score) / 100)
412
+ elif score < 46: signal, confidence = "SELL", min(0.68, (50 - score) / 50)
413
+ else: signal, confidence = "WAIT", 0.0
414
 
415
+ analysis = {
416
+ "derivative_score": score,
417
+ "score_zscore": round(score_z, 2),
418
+ "signal": signal,
419
+ "confidence": round(confidence, 4),
420
+ "signals_breakdown": {k: v[0] for k, v in signals.items()},
421
+ "metrics": {**metrics, "vix": vix_data, "max_pain": max_pain}
422
+ }
423
+
424
+ OPTIONS_HISTORY.append({
425
+ "timestamp": datetime.now(timezone.utc).isoformat(),
426
+ "symbol": symbol, "signal": signal, "score": score
427
+ })
428
  save_history()
429
+
430
  return analysis
431
 
432
+ # ================= ОТПРАВКА В HUB =================
433
  async def send_signal_to_hub(symbol: str, signal: str, confidence: float, features: Dict = None):
434
+ if features is None: features = {}
 
 
435
  payload = {
436
+ "space_id": SPACE_ID, "space_name": SPACE_NAME,
437
+ "symbol": symbol, "signal": signal, "confidence": round(confidence, 4),
438
+ "features": features, "metadata": {"version": "4.0"},
439
+ "timestamp": datetime.now(timezone.utc).isoformat()
 
 
 
 
440
  }
 
441
  for attempt in range(3):
442
  try:
443
+ r = await http_client.post(f"{HUB_URL}/signals", json=payload, timeout=15, headers=hub_headers())
 
444
  if r.status_code == 200:
445
  logger.info(f"📤 {symbol}: {signal} conf={confidence:.3f}")
446
+ return True
447
+ await asyncio.sleep(2)
 
 
 
 
 
 
448
  except Exception as e:
449
  logger.warning(f"Попытка {attempt+1}: {e}")
450
  await asyncio.sleep(2)
451
+ return False
 
452
 
453
  # ================= ГЛАВНЫЙ СИГНАЛ =================
454
  async def get_derivative_signal(symbol: str = "ETH/USD") -> Dict[str, Any]:
 
456
  analysis = await analyze_derivatives(symbol)
457
  latency = int((time.time() - start) * 1000)
458
 
459
+ features = {
460
+ "derivative_score": analysis['derivative_score'],
461
+ "score_zscore": analysis['score_zscore']
462
+ }
463
  await send_signal_to_hub(symbol, analysis['signal'], analysis['confidence'], features)
464
 
465
+ logger.info(f"📊 Options {symbol}: {analysis['signal']} conf={analysis['confidence']:.3f} score={analysis['derivative_score']} | {latency}ms")
466
  return {
467
+ "space_id": SPACE_ID, "timestamp": int(time.time()),
468
+ "symbol": symbol, "signal": analysis['signal'],
469
+ "confidence": analysis['confidence'], "derivative_analysis": analysis
 
 
 
470
  }
471
 
472
  # ================= АВТО-ОТПРАВКА =================
473
  async def auto_send_loop():
474
  logger.info(f"⏳ Стартовый сон {STARTUP_SLEEP}с...")
475
+ await log_to_hub("STARTUP", f"Options Engine v4.0 запущен, жду {STARTUP_SLEEP}с")
476
  await asyncio.sleep(STARTUP_SLEEP)
477
+ logger.info(f"🔄 Options Engine [интервал={AUTO_SEND_INTERVAL}с]")
478
  while True:
479
  try:
480
  for symbol in SYMBOLS:
 
482
  await asyncio.sleep(2)
483
  logger.info("✅ Options Engine цикл завершён")
484
  except Exception as e:
485
+ logger.error(f"Ошибка: {e}")
486
+ await log_to_hub("ERROR", f"Ошибка: {str(e)[:200]}")
487
  await asyncio.sleep(AUTO_SEND_INTERVAL)
488
 
489
  # ================= FASTAPI =================
490
+ app = FastAPI(title="Tomiris Space 26 v4.0 — Options & Derivatives")
491
 
492
  @app.on_event("startup")
493
  async def startup():
494
  asyncio.create_task(auto_send_loop())
495
+ logger.info(f"🚀 Space 26 v4.0 | Хаб: {HUB_URL}")
496
 
497
  @app.on_event("shutdown")
498
+ async def shutdown(): await http_client.aclose()
 
499
 
500
  @app.get("/health")
501
+ async def health(): return {"space_id": SPACE_ID, "status": "operational", "version": "4.0"}
 
 
502
  @app.head("/health")
503
+ async def health_head(): return {}
 
504
 
505
  @app.get("/consilium")
506
  async def consilium(symbol: str = Query("ETH/USD")):
 
508
  return await get_derivative_signal(symbol)
509
 
510
  @app.get("/funding/{symbol}")
511
+ async def funding(symbol: str):
512
+ native = "XAU" if "XAU" in symbol.upper() else ("ETH" if "ETH" in symbol.upper() else "SOL")
513
+ bin_sym = native + "USDT"
514
+ return await fetch_funding_rate(bin_sym)
515
+
516
  @app.get("/oi/{symbol}")
517
+ async def oi(symbol: str):
518
+ native = "XAU" if "XAU" in symbol.upper() else ("ETH" if "ETH" in symbol.upper() else "SOL")
519
+ bin_sym = native + "USDT"
520
+ return await fetch_open_interest(bin_sym)
521
+
522
  @app.get("/lsr/{symbol}")
523
+ async def lsr(symbol: str):
524
+ native = "XAU" if "XAU" in symbol.upper() else ("ETH" if "ETH" in symbol.upper() else "SOL")
525
+ bin_sym = native + "USDT"
526
+ return await fetch_long_short_ratio(bin_sym)
527
+
528
  @app.get("/options")
529
  async def options(): return await fetch_deribit_options("ETH")
530
+
531
  @app.get("/gold")
532
  async def gold(): return await fetch_gold_derivatives()
533
+
534
  @app.get("/vix")
535
  async def vix(): return await fetch_vix()
536
+
537
  @app.get("/maxpain/{symbol}")
538
  async def maxpain(symbol: str):
539
  price = await get_current_price(symbol); mp = calculate_max_pain(price)
540
  return {"symbol": symbol, "current_price": price, "max_pain": mp}
541
+
542
  @app.get("/history")
543
  async def history(limit: int = 50): return list(OPTIONS_HISTORY)[-limit:]
544
+
545
  @app.get("/send_now")
546
  async def send_now():
547
  results = {}
 
551
  return {"status": "sent", "results": results}
552
 
553
  @app.get("/")
554
+ async def root(): return {"name": "Options Engine v4.0", "space_id": SPACE_ID, "hub": HUB_URL}
 
555
 
556
  if __name__ == "__main__":
557
  import uvicorn
558
  uvicorn.run(app, host="0.0.0.0", port=7860)
559
 
560
+ print("🚀 SPACE 26 v4.0 — OPTIONS & DERIVATIVES ENGINE ГОТОВ!")