tomirisg25 commited on
Commit
804f78d
·
verified ·
1 Parent(s): b285a2d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +333 -301
app.py CHANGED
@@ -1,3 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  # ============================================
2
  # АВТО-УСТАНОВКА ПАКЕТОВ
3
  # ============================================
@@ -7,6 +52,7 @@ import importlib
7
 
8
  REQUIRED_PACKAGES = {
9
  'numpy': 'numpy',
 
10
  'requests': 'requests'
11
  }
12
 
@@ -19,22 +65,21 @@ for module_name, pip_name in REQUIRED_PACKAGES.items():
19
  print(f"✅ {pip_name} установлен!")
20
 
21
  # ============================================
22
- # 👑 TOMIRIS SPACE 27 v1.0 — SEASONALITY & CALENDAR EFFECTS
23
  # ============================================
24
- # Анализирует календарные закономерности:
25
- # Месячная сезонность (золото в сентябре, крипта в декабре).
26
- # Дни недели (понедельниккрасный для крипты).
27
- # Экспирации опционов (Max Pain притяжение).
28
- # Халвинги, налоговые периоды, праздники.
29
  # ============================================
30
 
31
  from fastapi import FastAPI, Query
32
- from typing import Dict, Any, List, Optional
33
  import time
34
  import requests
35
  import threading
36
  import numpy as np
37
- from datetime import datetime, timedelta
 
38
  from collections import deque
39
  import warnings
40
  warnings.filterwarnings('ignore')
@@ -44,297 +89,269 @@ SYMBOLS: List[str] = ["XAU/USD", "ETH/USD", "SOL/USD"]
44
 
45
  SPACE_18_ARBITER: str = "https://tomiris-ai-name6-6.hf.space"
46
 
47
- CACHE: Dict[str, Dict[str, Any]] = {}
 
 
 
 
 
48
  CACHE_TIMES: Dict[str, float] = {}
49
  FEATURES_STORE: Dict[str, Dict[str, Any]] = {}
50
  MT5_MAX_AGE_SEC: int = 300
 
51
 
52
- # ================= ИСТОРИЧЕСКАЯ СЕЗОННОСТЬ =================
53
- # Средняя месячная доходность в % (аппроксимация на основе истории)
54
- MONTHLY_SEASONALITY: Dict[str, Dict[int, float]] = {
55
- "XAU/USD": {
56
- 1: 0.8, 2: 0.3, 3: -0.2, 4: 0.5, 5: 0.2,
57
- 6: -0.5, 7: 0.4, 8: 1.2, 9: 2.5, 10: 0.6,
58
- 11: 0.9, 12: 1.5
59
- },
60
- "ETH/USD": {
61
- 1: 2.0, 2: 1.5, 3: 3.0, 4: -1.0, 5: 5.0,
62
- 6: -2.0, 7: 3.0, 8: -1.5, 9: -3.0, 10: 2.0,
63
- 11: 1.0, 12: 4.0
64
- },
65
- "SOL/USD": {
66
- 1: 3.0, 2: 2.0, 3: 5.0, 4: -2.0, 5: 6.0,
67
- 6: -3.0, 7: 4.0, 8: -2.0, 9: -4.0, 10: 3.0,
68
- 11: 2.0, 12: 5.0
69
- }
70
- }
71
-
72
- # Доходность по дням недели (средняя)
73
- DAILY_SEASONALITY: Dict[str, Dict[int, float]] = {
74
- "XAU/USD": {0: 0.05, 1: 0.02, 2: -0.03, 3: 0.04, 4: -0.08, 5: 0.0, 6: 0.0},
75
- "ETH/USD": {0: -0.15, 1: 0.10, 2: 0.05, 3: 0.08, 4: 0.12, 5: -0.05, 6: -0.10},
76
- "SOL/USD": {0: -0.20, 1: 0.15, 2: 0.08, 3: 0.10, 4: 0.15, 5: -0.08, 6: -0.12}
77
- }
78
 
79
- # Ключевые даты экспираций (2026)
80
- OPTION_EXPIRY_DATES: List[str] = [
81
- "2026-06-26", "2026-07-31", "2026-08-28", "2026-09-25",
82
- "2026-10-30", "2026-11-27", "2026-12-25"
83
- ]
 
84
 
85
  def send_to_arbiter(signal_data: Dict[str, Any]) -> None:
86
  try:
87
  requests.post(
88
  f"{SPACE_18_ARBITER}/log_signal",
89
- json={'space': 'space_27_seasonality', 'symbol': 'ALL', 'signal': signal_data.get('signal', {})},
90
  timeout=5
91
  )
92
  except:
93
  pass
94
 
95
- # ================= МЕСЯЧНАЯ СЕЗОННОСТЬ =================
96
- def analyze_monthly_seasonality(symbol: str) -> Dict[str, Any]:
97
- now = datetime.utcnow()
98
- current_month = now.month
99
-
100
- seasonality = MONTHLY_SEASONALITY.get(symbol, {})
101
- current_return = seasonality.get(current_month, 0)
102
-
103
- # Сигнал на основе ожидаемой доходности
104
- if current_return > 1.5:
105
- seasonal_signal = "STRONG_BULLISH"
106
- bias = 0.15
107
- elif current_return > 0.5:
108
- seasonal_signal = "BULLISH"
109
- bias = 0.10
110
- elif current_return < -1.5:
111
- seasonal_signal = "STRONG_BEARISH"
112
- bias = -0.15
113
- elif current_return < -0.5:
114
- seasonal_signal = "BEARISH"
115
- bias = -0.10
116
- else:
117
- seasonal_signal = "NEUTRAL"
118
- bias = 0.0
119
 
120
- # Лучшие и худшие месяцы
121
- sorted_months = sorted(seasonality.items(), key=lambda x: x[1], reverse=True)
122
- best_months = [m for m, _ in sorted_months[:3]]
123
- worst_months = [m for m, _ in sorted_months[-3:]]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
 
125
- return {
126
- "current_month": current_month,
127
- "month_name": now.strftime("%B"),
128
- "expected_return_pct": round(current_return, 2),
129
- "seasonal_signal": seasonal_signal,
130
- "bias": bias,
131
- "best_months": best_months,
132
- "worst_months": worst_months
133
- }
134
 
135
- # ================= ДНИ НЕДЕЛИ =================
136
- def analyze_daily_seasonality(symbol: str) -> Dict[str, Any]:
137
- now = datetime.utcnow()
138
- weekday = now.weekday()
139
 
140
- daily = DAILY_SEASONALITY.get(symbol, {})
141
- today_return = daily.get(weekday, 0)
 
 
142
 
143
- if today_return > 0.10:
144
- daily_signal = "BULLISH"
145
- bias = 0.05
146
- elif today_return < -0.10:
147
- daily_signal = "BEARISH"
148
- bias = -0.05
149
- else:
150
- daily_signal = "NEUTRAL"
151
- bias = 0.0
152
 
153
- day_names = ["Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота", "Воскресенье"]
 
 
154
 
155
- return {
156
- "weekday": weekday,
157
- "day_name": day_names[weekday],
158
- "expected_return_pct": round(today_return, 3),
159
- "daily_signal": daily_signal,
160
- "bias": bias,
161
- "is_weekend": weekday >= 5,
162
- "weekend_note": "Рынок XAU закрыт" if weekday >= 5 else None
163
- }
164
-
165
- # ================= ЭКСПИРАЦИИ ОПЦИОНОВ =================
166
- def analyze_option_expiry() -> Dict[str, Any]:
167
- now = datetime.utcnow()
168
- today = now.strftime("%Y-%m-%d")
169
- hour = now.hour
170
- days_until_expiry = None
171
- nearest_expiry = None
172
-
173
- for expiry in OPTION_EXPIRY_DATES:
174
- expiry_date = datetime.strptime(expiry, "%Y-%m-%d")
175
- diff = (expiry_date - now).days
176
-
177
- if diff >= 0 and (days_until_expiry is None or diff < days_until_expiry):
178
- days_until_expiry = diff
179
- nearest_expiry = expiry
180
-
181
- if days_until_expiry is not None:
182
- if days_until_expiry == 0:
183
- expiry_signal = "EXPIRY_TODAY"
184
- note = "Максимальная волатильность, притяжение к Max Pain"
185
- bias = 0.0
186
- elif days_until_expiry <= 2:
187
- expiry_signal = "EXPIRY_SOON"
188
- note = f"Экспирация через {days_until_expiry} дн. — притяжение к страйкам"
189
- bias = -0.05
190
- elif days_until_expiry <= 5:
191
- expiry_signal = "EXPIRY_WEEK"
192
- note = f"Неделя экспирации — повышенная волатильность"
193
- bias = 0.0
194
- else:
195
- expiry_signal = "NORMAL"
196
- note = None
197
- bias = 0.0
198
  else:
199
- expiry_signal = "NORMAL"
200
- note = None
201
- bias = 0.0
202
-
203
- return {
204
- "nearest_expiry": nearest_expiry,
205
- "days_until_expiry": days_until_expiry,
206
- "expiry_signal": expiry_signal,
207
- "note": note,
208
- "bias": bias
209
- }
210
-
211
- # ================= ПРАЗДНИКИ / ОСОБЫЕ ДНИ =================
212
- def analyze_special_days() -> Dict[str, Any]:
213
- now = datetime.utcnow()
214
- month = now.month
215
- day = now.day
216
-
217
- specials = []
218
-
219
- # Налоговый сезон США (апрель)
220
- if month == 4 and day <= 15:
221
- specials.append({"event": "TAX_SEASON_US", "impact": "BEARISH", "note": "Продажи для уплаты налогов"})
222
-
223
- # Рождественское ралли (декабрь)
224
- if month == 12 and day >= 20:
225
- specials.append({"event": "SANTA_CLAUS_RALLY", "impact": "BULLISH", "note": "Исторически позитивный период"})
226
-
227
- # Сезон свадеб в Индии (октябрь-декабрь) — спрос на золото
228
- if month in [10, 11, 12]:
229
- specials.append({"event": "INDIAN_WEDDING_SEASON", "impact": "BULLISH", "note": "Повышенный спрос на золото"})
230
-
231
- # Китайский Новый год (конец января — февраль)
232
- if (month == 1 and day >= 20) or (month == 2 and day <= 10):
233
- specials.append({"event": "CHINESE_NEW_YEAR", "impact": "BULLISH", "note": "Праздничный спрос на золото"})
234
-
235
- # Летнее затишье (июль-август)
236
- if month in [7, 8]:
237
- specials.append({"event": "SUMMER_LULL", "impact": "NEUTRAL", "note": "Снижение объёмов"})
238
-
239
- if specials:
240
- total_bias = sum(0.05 if s['impact'] == 'BULLISH' else -0.05 if s['impact'] == 'BEARISH' else 0 for s in specials)
241
  else:
242
- total_bias = 0.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
 
244
  return {
245
- "specials": specials,
246
- "has_specials": len(specials) > 0,
247
- "bias": total_bias
 
 
 
 
 
 
 
248
  }
249
 
250
  # ================= АНАЛИЗ ИЗ MT5 =================
251
  def analyze_from_mt5(mt5_features: Dict[str, Any]) -> Optional[Dict[str, Any]]:
252
  try:
253
- now = datetime.utcnow()
254
- hour = now.hour
255
- weekday = now.weekday()
256
-
257
  score = 50.0
258
-
259
- if 13 <= hour < 22:
260
- score += 5
261
- elif hour < 8:
262
- score -= 5
263
- if weekday == 4:
264
- score -= 5
265
- if weekday >= 5:
266
- score -= 10
267
-
268
- score = max(0, min(100, score))
269
-
270
- if score > 55:
271
- direction, confidence = "LONG", score / 100
272
- elif score < 45:
273
- direction, confidence = "SHORT", (100 - score) / 100
274
- else:
275
- direction, confidence = "WAIT", 0.0
 
 
 
276
 
277
  return {
278
- "direction": direction,
279
- "confidence": round(confidence, 4),
280
- "calendar_score": score,
 
281
  "source": "MT5"
282
  }
283
  except:
284
  return None
285
 
286
- # ================= ГЛАВНЫЙ АНАЛИЗ =================
287
- def analyze_seasonality(symbol: str) -> Dict[str, Any]:
288
- monthly = analyze_monthly_seasonality(symbol)
289
- daily = analyze_daily_seasonality(symbol)
290
- expiry = analyze_option_expiry()
291
- specials = analyze_special_days()
292
-
293
- # Суммируем bias'ы
294
- total_bias = monthly['bias'] + daily['bias'] + expiry['bias'] + specials['bias']
295
-
296
- # Базовый скор
297
- score = 50.0 + total_bias * 100
298
- score = max(0, min(100, score))
299
-
300
- if score > 55:
301
- direction = "LONG"
302
- confidence = score / 100
303
- elif score < 45:
304
- direction = "SHORT"
305
- confidence = (100 - score) / 100
306
- else:
307
- direction = "WAIT"
308
- confidence = 0.0
309
-
310
- signals = []
311
- if monthly['seasonal_signal'] != 'NEUTRAL':
312
- signals.append({"factor": "MONTHLY", "signal": monthly['seasonal_signal'], "reason": f"{monthly['month_name']}: {monthly['expected_return_pct']:+.1f}%"})
313
- if daily['daily_signal'] != 'NEUTRAL':
314
- signals.append({"factor": "DAILY", "signal": daily['daily_signal'], "reason": f"{daily['day_name']}: {daily['expected_return_pct']:+.2f}%"})
315
- if expiry['expiry_signal'] != 'NORMAL':
316
- signals.append({"factor": "EXPIRY", "signal": expiry['expiry_signal'], "reason": expiry.get('note', '')})
317
- for s in specials.get('specials', []):
318
- signals.append({"factor": s['event'], "signal": s['impact'], "reason": s['note']})
319
-
320
- return {
321
- "seasonality_score": round(score, 2),
322
- "direction": direction,
323
- "confidence": round(confidence, 4),
324
- "total_bias": round(total_bias, 4),
325
- "signals": signals,
326
- "components": {
327
- "monthly": monthly,
328
- "daily": daily,
329
- "expiry": expiry,
330
- "specials": specials
331
- }
332
- }
333
-
334
  # ================= ГЛАВНЫЙ СИГНАЛ =================
335
- def get_seasonality_signal(symbol: str = "XAU/USD") -> Dict[str, Any]:
336
  start = time.time()
337
 
 
338
  if symbol in FEATURES_STORE:
339
  fs = FEATURES_STORE[symbol]
340
  age = time.time() - fs.get("timestamp", 0)
@@ -343,48 +360,55 @@ def get_seasonality_signal(symbol: str = "XAU/USD") -> Dict[str, Any]:
343
  if mt5_features:
344
  mt5_result = analyze_from_mt5(mt5_features)
345
  if mt5_result:
 
346
  result = {
347
- "space": "space_27_seasonality",
348
  "timestamp": int(time.time()),
349
  "symbol": symbol,
350
  "signal": {
351
- "direction": mt5_result["direction"],
352
- "confidence": mt5_result["confidence"]
 
353
  },
354
- "seasonality_analysis": {
355
- "score": mt5_result["calendar_score"],
356
- "signals": [{"factor": "MT5", "signal": mt5_result["direction"], "reason": "Технический"}]
 
 
357
  },
358
  "data_source": "MT5",
359
  "meta": {"latency_ms": int((time.time() - start) * 1000)}
360
  }
361
  send_to_arbiter(result)
362
- print(f"📅 SEASON {symbol}: {mt5_result['direction']} | Score={mt5_result['calendar_score']} (MT5)")
363
  return result
364
 
365
- analysis = analyze_seasonality(symbol)
 
 
 
 
 
 
366
  latency = int((time.time() - start) * 1000)
367
 
368
  result = {
369
- "space": "space_27_seasonality",
370
  "timestamp": int(time.time()),
371
  "symbol": symbol,
372
  "signal": {
373
- "direction": analysis['direction'],
374
- "confidence": analysis['confidence']
375
- },
376
- "seasonality_analysis": {
377
- "score": analysis['seasonality_score'],
378
- "total_bias": analysis['total_bias'],
379
- "signals": analysis['signals'],
380
- "components": analysis['components']
381
  },
382
- "data_source": "CALENDAR",
 
383
  "meta": {"latency_ms": latency}
384
  }
385
 
386
  send_to_arbiter(result)
387
- print(f"📅 SEASON v1 {symbol}: {analysis['direction']} | Bias={analysis['total_bias']:.3f}")
388
  return result
389
 
390
  # ================= KEEP-ALIVE =================
@@ -399,49 +423,57 @@ def keep_alive():
399
  threading.Thread(target=keep_alive, daemon=True).start()
400
 
401
  # ================= FASTAPI =================
402
- app = FastAPI(title="TOMIRIS SPACE 27 v1.0 — SEASONALITY & CALENDAR EFFECTS")
403
 
404
  @app.get("/health")
405
  @app.head("/health")
406
  async def health():
407
  return {
408
- "space": "Space 27 - Seasonality & Calendar Effects v1.0",
409
  "status": "operational",
410
  "symbols": SYMBOLS,
411
- "features": ["Monthly Seasonality", "Daily Patterns", "Option Expiry", "Special Days"]
 
412
  }
413
 
414
  @app.get("/consilium")
415
  async def consilium(symbol: str = Query("XAU/USD")):
416
  if symbol not in SYMBOLS:
417
  return {"error": f"Unsupported: {symbol}"}
418
- return get_seasonality_signal(symbol)
419
 
420
- @app.get("/monthly/{symbol}")
421
- async def monthly(symbol: str):
422
  if symbol not in SYMBOLS:
423
  return {"error": f"Unsupported: {symbol}"}
424
- return analyze_monthly_seasonality(symbol)
 
 
 
425
 
426
- @app.get("/daily/{symbol}")
427
- async def daily(symbol: str):
428
  if symbol not in SYMBOLS:
429
  return {"error": f"Unsupported: {symbol}"}
430
- return analyze_daily_seasonality(symbol)
431
-
432
- @app.get("/expiry")
433
- async def expiry():
434
- return analyze_option_expiry()
435
-
436
- @app.get("/specials")
437
- async def specials():
438
- return analyze_special_days()
439
 
440
- @app.get("/full/{symbol}")
441
- async def full(symbol: str):
442
- if symbol not in SYMBOLS:
443
- return {"error": f"Unsupported: {symbol}"}
444
- return analyze_seasonality(symbol)
 
 
 
 
 
 
445
 
446
  @app.post("/features")
447
  async def receive_features(data: Dict[str, Any]):
@@ -454,6 +486,6 @@ async def receive_features(data: Dict[str, Any]):
454
  print(f"📥 MT5 {symbol}: {len(data.get('features', {}))} признаков")
455
  return {"status": "ok"}
456
 
457
- print(f"🚀 SPACE 27 v1.0 — SEASONALITY & CALENDAR EFFECTS ЗАПУЩЕН!")
458
- print(f"📅 Анализ: Месяцы | Дни недели | Экспирации | Праздники | Налоговый сезон")
459
  print(f"✅ Готов к бою!")
 
1
+ Hugging Face's logo
2
+ Hugging Face
3
+ Models
4
+ Datasets
5
+ Spaces
6
+ Buckets
7
+ new
8
+ Docs
9
+ Pricing
10
+
11
+ Website
12
+ Community
13
+ Solutions
14
+
15
+ Spaces:
16
+ tomirisg25
17
+ /
18
+ TomirisGold5
19
+
20
+
21
+ like
22
+ 0
23
+
24
+ App
25
+ Files
26
+ Community
27
+ Settings
28
+ TomirisGold5
29
+ /
30
+ app.py
31
+
32
+ tomirisg25's picture
33
+ tomirisg25
34
+ Create app.py
35
+ b7c9e15
36
+ verified
37
+ about 18 hours ago
38
+ raw
39
+
40
+ Copy download link
41
+ history
42
+ blame
43
+ edit
44
+ delete
45
+ 14.7 kB
46
  # ============================================
47
  # АВТО-УСТАНОВКА ПАКЕТОВ
48
  # ============================================
 
52
 
53
  REQUIRED_PACKAGES = {
54
  'numpy': 'numpy',
55
+ 'pandas': 'pandas',
56
  'requests': 'requests'
57
  }
58
 
 
65
  print(f"✅ {pip_name} установлен!")
66
 
67
  # ============================================
68
+ # 👑 TOMIRIS SPACE 28 v1.0 — MARKET REGIME & BUBBLE DETECTOR
69
  # ============================================
70
+ # Определяет режим рынка (TREND/RANGE/VOLATILE/BUBBLE).
71
+ # Euphoria Index, NVT Ratio, RSI экстремумы, пузыри.
72
+ # Имеет право ВЕТО при пузыре блокирует BUY.
 
 
73
  # ============================================
74
 
75
  from fastapi import FastAPI, Query
76
+ from typing import Dict, Any, List, Optional, Tuple
77
  import time
78
  import requests
79
  import threading
80
  import numpy as np
81
+ import pandas as pd
82
+ from datetime import datetime
83
  from collections import deque
84
  import warnings
85
  warnings.filterwarnings('ignore')
 
89
 
90
  SPACE_18_ARBITER: str = "https://tomiris-ai-name6-6.hf.space"
91
 
92
+ TWELVE_KEYS: List[str] = [
93
+ "e3740c072fda4fe8b8539d40b07e445e",
94
+ "58e67e0008e24161ac9b1671b7c2d2d0"
95
+ ]
96
+
97
+ CACHE: Dict[str, Any] = {}
98
  CACHE_TIMES: Dict[str, float] = {}
99
  FEATURES_STORE: Dict[str, Dict[str, Any]] = {}
100
  MT5_MAX_AGE_SEC: int = 300
101
+ BUBBLE_HISTORY: deque = deque(maxlen=200)
102
 
103
+ twelve_counter: int = 0
104
+ api_lock = threading.Lock()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
 
106
+ def get_next_key() -> str:
107
+ global twelve_counter
108
+ with api_lock:
109
+ key = TWELVE_KEYS[twelve_counter % len(TWELVE_KEYS)]
110
+ twelve_counter += 1
111
+ return key
112
 
113
  def send_to_arbiter(signal_data: Dict[str, Any]) -> None:
114
  try:
115
  requests.post(
116
  f"{SPACE_18_ARBITER}/log_signal",
117
+ json={'space': 'space_28_regime', 'symbol': 'ALL', 'signal': signal_data.get('signal', {})},
118
  timeout=5
119
  )
120
  except:
121
  pass
122
 
123
+ # ================= ЗАГРУЗКА ДАННЫХ =================
124
+ def fetch_historical(symbol: str, tf: str = "1h", count: int = 200) -> Optional[pd.DataFrame]:
125
+ cache_key = f"hist_{symbol}_{tf}_{count}"
126
+ if cache_key in CACHE and time.time() - CACHE_TIMES.get(cache_key, 0) < 300:
127
+ return CACHE[cache_key]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
+ try:
130
+ key = get_next_key()
131
+ twelve_symbol = symbol.replace("/", "")
132
+ url = f"https://api.twelvedata.com/time_series?symbol={twelve_symbol}&interval={tf}&outputsize={count}&apikey={key}"
133
+ r = requests.get(url, timeout=10)
134
+ if r.status_code == 200:
135
+ data = r.json()
136
+ if 'values' in data:
137
+ df = pd.DataFrame(data['values']).iloc[::-1]
138
+ df['close'] = pd.to_numeric(df['close'])
139
+ df['high'] = pd.to_numeric(df['high'])
140
+ df['low'] = pd.to_numeric(df['low'])
141
+ if 'volume' in df.columns:
142
+ df['volume'] = pd.to_numeric(df['volume'], errors='coerce').fillna(0)
143
+ CACHE[cache_key] = df
144
+ CACHE_TIMES[cache_key] = time.time()
145
+ return df
146
+ except Exception as e:
147
+ print(f"⚠️ {symbol}: {e}")
148
+ return None
149
+
150
+ # ================= ИНДИКАТОРЫ =================
151
+ def safe_rsi(close: pd.Series, period: int = 14) -> float:
152
+ try:
153
+ delta = close.diff()
154
+ gain = delta.clip(lower=0).rolling(period, min_periods=period).mean()
155
+ loss = (-delta.clip(upper=0)).rolling(period, min_periods=period).mean()
156
+ g_val, l_val = gain.iloc[-1], loss.iloc[-1]
157
+ if pd.notna(g_val) and pd.notna(l_val) and l_val > 0:
158
+ rs = g_val / l_val
159
+ return float(100 - (100 / (1 + rs)))
160
+ return 50.0
161
+ except:
162
+ return 50.0
163
+
164
+ def calculate_euphoria_index(df: pd.DataFrame) -> float:
165
+ """Euphoria Index: 0-100, где >70 = эйфория."""
166
+ if df is None or len(df) < 50:
167
+ return 50.0
168
+
169
+ close = df['close']
170
+ volume = df['volume'] if 'volume' in df.columns else pd.Series([1]*len(df))
171
+
172
+ score = 0.0
173
+
174
+ # RSI на D1 (перекупленность)
175
+ rsi = safe_rsi(close, 14)
176
+ if rsi > 80:
177
+ score += 30
178
+ elif rsi > 70:
179
+ score += 20
180
+ elif rsi > 60:
181
+ score += 10
182
+
183
+ # Положение относительно SMA 50
184
+ if len(close) >= 50:
185
+ sma50 = close.rolling(50).mean().iloc[-1]
186
+ price_vs_sma = ((close.iloc[-1] - sma50) / sma50) * 100
187
+ if price_vs_sma > 20:
188
+ score += 25
189
+ elif price_vs_sma > 10:
190
+ score += 15
191
+ elif price_vs_sma > 5:
192
+ score += 5
193
 
194
+ # Объём (аномально высокий = эйфория)
195
+ if len(volume) >= 20:
196
+ vol_avg = volume.rolling(20).mean().iloc[-1]
197
+ vol_ratio = volume.iloc[-1] / (vol_avg + 1e-10)
198
+ if vol_ratio > 3:
199
+ score += 20
200
+ elif vol_ratio > 2:
201
+ score += 10
 
202
 
203
+ return min(100, score)
 
 
 
204
 
205
+ def calculate_nvt_ratio(df: pd.DataFrame) -> float:
206
+ """Network Value to Transactions — аналог NVT для крипты."""
207
+ if df is None or len(df) < 30:
208
+ return 50.0
209
 
210
+ close = df['close']
211
+ volume = df['volume'] if 'volume' in df.columns else pd.Series([1]*len(df))
 
 
 
 
 
 
 
212
 
213
+ # Упрощённый NVT = MarketCap / Daily Volume
214
+ market_cap = close.iloc[-1] * 120_000_000 # Грубая оценка supply
215
+ daily_volume = volume.iloc[-24:].sum() if len(volume) >= 24 else volume.sum()
216
 
217
+ if daily_volume > 0:
218
+ nvt = market_cap / daily_volume
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
  else:
220
+ nvt = 50
221
+
222
+ # Нормализация (для ETH норма NVT ~ 30-80)
223
+ if nvt > 150:
224
+ nvt_signal = "EXTREME_OVERBOUGHT"
225
+ nvt_score = 30
226
+ elif nvt > 100:
227
+ nvt_signal = "OVERBOUGHT"
228
+ nvt_score = 20
229
+ elif nvt < 30:
230
+ nvt_signal = "OVERSOLD"
231
+ nvt_score = -15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  else:
233
+ nvt_signal = "NORMAL"
234
+ nvt_score = 0
235
+
236
+ return float(nvt_score)
237
+
238
+ # ================= РЕЖИМ РЫНКА =================
239
+ def detect_market_regime(df: pd.DataFrame, symbol: str) -> Dict[str, Any]:
240
+ if df is None or len(df) < 50:
241
+ return {"regime": "UNKNOWN", "score": 50, "bubble_risk": "UNKNOWN", "veto": False}
242
+
243
+ close = df['close'].values
244
+ high = df['high'].values
245
+ low = df['low'].values
246
+
247
+ # Волатильность
248
+ returns = np.diff(np.log(close))
249
+ volatility = float(np.std(returns[-24:])) if len(returns) >= 24 else 0.01
250
+
251
+ # ADX (упрощённо через размах)
252
+ recent_high = np.max(high[-20:])
253
+ recent_low = np.min(low[-20:])
254
+ range_pct = (recent_high - recent_low) / recent_low * 100
255
+
256
+ # Euphoria Index
257
+ euphoria = calculate_euphoria_index(df)
258
+
259
+ # NVT (только для крипты)
260
+ nvt_score = 0.0
261
+ if "XAU" not in symbol:
262
+ nvt_score = calculate_nvt_ratio(df)
263
+
264
+ # RSI
265
+ rsi = safe_rsi(pd.Series(close), 14)
266
+
267
+ # Опред��ление режима
268
+ if euphoria > 70:
269
+ regime = "BUBBLE"
270
+ veto = True
271
+ signal = "FORCE_WAIT"
272
+ elif euphoria > 55:
273
+ regime = "EUPHORIA"
274
+ veto = False
275
+ signal = "CAUTION"
276
+ elif volatility > 0.03:
277
+ regime = "VOLATILE"
278
+ veto = False
279
+ signal = "NEUTRAL"
280
+ elif range_pct < 3:
281
+ regime = "RANGE"
282
+ veto = False
283
+ signal = "NEUTRAL"
284
+ else:
285
+ regime = "TREND"
286
+ veto = False
287
+ signal = "NORMAL"
288
+
289
+ regime_score = 50.0
290
+ if regime == "BUBBLE":
291
+ regime_score = 90
292
+ elif regime == "EUPHORIA":
293
+ regime_score = 70
294
+ elif regime == "VOLATILE":
295
+ regime_score = 55
296
+ elif regime == "RANGE":
297
+ regime_score = 30
298
+ else:
299
+ regime_score = 50
300
 
301
  return {
302
+ "regime": regime,
303
+ "regime_score": regime_score,
304
+ "euphoria_index": round(euphoria, 1),
305
+ "nvt_score": round(nvt_score, 1),
306
+ "rsi_14": round(rsi, 1),
307
+ "volatility_24h_pct": round(volatility * 100, 3),
308
+ "range_20_pct": round(range_pct, 1),
309
+ "bubble_risk": "HIGH" if regime == "BUBBLE" else "ELEVATED" if regime == "EUPHORIA" else "LOW",
310
+ "veto": veto,
311
+ "signal": signal
312
  }
313
 
314
  # ================= АНАЛИЗ ИЗ MT5 =================
315
  def analyze_from_mt5(mt5_features: Dict[str, Any]) -> Optional[Dict[str, Any]]:
316
  try:
 
 
 
 
317
  score = 50.0
318
+ veto = False
319
+
320
+ rsi = mt5_features.get('H1_rsi', 50)
321
+ if isinstance(rsi, (int, float)):
322
+ if rsi > 85:
323
+ veto = True
324
+ regime = "BUBBLE"
325
+ score = 90
326
+ elif rsi > 75:
327
+ regime = "EUPHORIA"
328
+ score = 70
329
+ elif rsi < 20:
330
+ regime = "CAPITULATION"
331
+ score = 10
332
+ else:
333
+ regime = "NORMAL"
334
+
335
+ atr_pct = mt5_features.get('H1_atr_pct', 1)
336
+ if isinstance(atr_pct, (int, float)) and atr_pct > 4:
337
+ regime = "VOLATILE"
338
+ score = max(score, 60)
339
 
340
  return {
341
+ "regime": regime,
342
+ "regime_score": score,
343
+ "veto": veto,
344
+ "euphoria_index": score,
345
  "source": "MT5"
346
  }
347
  except:
348
  return None
349
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
350
  # ================= ГЛАВНЫЙ СИГНАЛ =================
351
+ def get_regime_signal(symbol: str = "XAU/USD") -> Dict[str, Any]:
352
  start = time.time()
353
 
354
+ # Проверка MT5
355
  if symbol in FEATURES_STORE:
356
  fs = FEATURES_STORE[symbol]
357
  age = time.time() - fs.get("timestamp", 0)
 
360
  if mt5_features:
361
  mt5_result = analyze_from_mt5(mt5_features)
362
  if mt5_result:
363
+ direction = "WAIT" if mt5_result["veto"] else "NEUTRAL"
364
  result = {
365
+ "space": "space_28_regime",
366
  "timestamp": int(time.time()),
367
  "symbol": symbol,
368
  "signal": {
369
+ "direction": direction,
370
+ "confidence": mt5_result["regime_score"] / 100,
371
+ "veto": mt5_result["veto"]
372
  },
373
+ "regime_analysis": {
374
+ "regime": mt5_result["regime"],
375
+ "regime_score": mt5_result["regime_score"],
376
+ "euphoria_index": mt5_result["euphoria_index"],
377
+ "veto": mt5_result["veto"]
378
  },
379
  "data_source": "MT5",
380
  "meta": {"latency_ms": int((time.time() - start) * 1000)}
381
  }
382
  send_to_arbiter(result)
383
+ print(f"🫧 REGIME {symbol}: {mt5_result['regime']} | Veto={mt5_result['veto']} (MT5)")
384
  return result
385
 
386
+ # Fallback
387
+ df = fetch_historical(symbol, "1h", 200)
388
+ regime_data = detect_market_regime(df, symbol)
389
+
390
+ direction = "WAIT" if regime_data["veto"] else "NEUTRAL"
391
+ confidence = regime_data["regime_score"] / 100
392
+
393
  latency = int((time.time() - start) * 1000)
394
 
395
  result = {
396
+ "space": "space_28_regime",
397
  "timestamp": int(time.time()),
398
  "symbol": symbol,
399
  "signal": {
400
+ "direction": direction,
401
+ "confidence": round(confidence, 4),
402
+ "veto": regime_data["veto"],
403
+ "veto_reason": "BUBBLE_DETECTED" if regime_data["veto"] else None
 
 
 
 
404
  },
405
+ "regime_analysis": regime_data,
406
+ "data_source": "API",
407
  "meta": {"latency_ms": latency}
408
  }
409
 
410
  send_to_arbiter(result)
411
+ print(f"🫧 REGIME v1 {symbol}: {regime_data['regime']} | Euphoria={regime_data['euphoria_index']:.0f} | Veto={regime_data['veto']}")
412
  return result
413
 
414
  # ================= KEEP-ALIVE =================
 
423
  threading.Thread(target=keep_alive, daemon=True).start()
424
 
425
  # ================= FASTAPI =================
426
+ app = FastAPI(title="TOMIRIS SPACE 28 v1.0 — MARKET REGIME & BUBBLE DETECTOR")
427
 
428
  @app.get("/health")
429
  @app.head("/health")
430
  async def health():
431
  return {
432
+ "space": "Space 28 - Market Regime & Bubble Detector v1.0",
433
  "status": "operational",
434
  "symbols": SYMBOLS,
435
+ "features": ["Euphoria Index", "NVT Ratio", "RSI Extremes", "Bubble VETO"],
436
+ "bubble_history": len(BUBBLE_HISTORY)
437
  }
438
 
439
  @app.get("/consilium")
440
  async def consilium(symbol: str = Query("XAU/USD")):
441
  if symbol not in SYMBOLS:
442
  return {"error": f"Unsupported: {symbol}"}
443
+ return get_regime_signal(symbol)
444
 
445
+ @app.get("/regime/{symbol}")
446
+ async def regime(symbol: str):
447
  if symbol not in SYMBOLS:
448
  return {"error": f"Unsupported: {symbol}"}
449
+ df = fetch_historical(symbol)
450
+ if df is None:
451
+ return {"error": "no_data"}
452
+ return detect_market_regime(df, symbol)
453
 
454
+ @app.get("/euphoria/{symbol}")
455
+ async def euphoria(symbol: str):
456
  if symbol not in SYMBOLS:
457
  return {"error": f"Unsupported: {symbol}"}
458
+ df = fetch_historical(symbol)
459
+ if df is None:
460
+ return {"error": "no_data"}
461
+ return {
462
+ "symbol": symbol,
463
+ "euphoria_index": calculate_euphoria_index(df)
464
+ }
 
 
465
 
466
+ @app.get("/nvt/{symbol}")
467
+ async def nvt(symbol: str):
468
+ if symbol not in ["ETH/USD", "SOL/USD"]:
469
+ return {"error": "NVT доступен только для крипты"}
470
+ df = fetch_historical(symbol)
471
+ if df is None:
472
+ return {"error": "no_data"}
473
+ return {
474
+ "symbol": symbol,
475
+ "nvt_score": calculate_nvt_ratio(df)
476
+ }
477
 
478
  @app.post("/features")
479
  async def receive_features(data: Dict[str, Any]):
 
486
  print(f"📥 MT5 {symbol}: {len(data.get('features', {}))} признаков")
487
  return {"status": "ok"}
488
 
489
+ print(f"🚀 SPACE 28 v1.0 — MARKET REGIME & BUBBLE DETECTOR ЗАПУЩЕН!")
490
+ print(f"🫧 Детектор: Euphoria Index | NVT Ratio | RSI | Режим рынка | ВЕТО при пузыре")
491
  print(f"✅ Готов к бою!")