tomirisai80 commited on
Commit
eb0cbd9
·
verified ·
1 Parent(s): ac30aa1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +175 -361
app.py CHANGED
@@ -1,13 +1,14 @@
1
  # ============================================
2
  # АВТО-УСТАНОВКА ПАКЕТОВ
3
  # ============================================
4
- import subprocess
5
- import sys
6
- import importlib
7
 
8
  REQUIRED_PACKAGES = {
9
  'numpy': 'numpy',
10
  'pandas': 'pandas',
 
 
 
11
  'requests': 'requests'
12
  }
13
 
@@ -20,104 +21,48 @@ for module_name, pip_name in REQUIRED_PACKAGES.items():
20
  print(f"✅ {pip_name} установлен!")
21
 
22
  # ============================================
23
- # 👑 TOMIRIS SPACE 21 v1.0 — GOLD MACRO & FLOW ANALYZER
24
  # ============================================
25
- # Специализируется на золоте: ETF-потоки (GLD/IAU/GDX),
26
- # COT-отчёты (Commitment of Traders), DXY, TIPS, GPR, потоки капитала.
27
- # Сигнализирует о притоке/оттоке "умных денег" в золото.
28
- # ============================================
29
-
30
- from fastapi import FastAPI, Query
31
- from typing import Optional, Dict, Any, List, Tuple
32
- import time
33
- import requests
34
- import threading
35
  import numpy as np
36
  import pandas as pd
37
- from datetime import datetime
38
- from collections import deque
39
- import warnings
40
- warnings.filterwarnings('ignore')
 
41
 
42
  # ================= КОНФИГУРАЦИЯ =================
43
- SYMBOL: str = "XAU/USD"
44
-
45
- # ETF тикеры для отслеживания
46
- GOLD_ETFS: Dict[str, str] = {
47
- "GLD": "GLD", # SPDR Gold Trust (крупнейший)
48
- "IAU": "IAU", # iShares Gold Trust
49
- "GDX": "GDX", # Gold Miners ETF
50
- "GDXJ": "GDXJ" # Junior Gold Miners
51
- }
52
 
53
- SPACE_18_ARBITER: str = "https://tomiris-ai-name6-6.hf.space"
54
-
55
- # API ключи
56
- TWELVE_KEYS: List[str] = [
57
- "f33e660d8c1945d19e3c4ded72a2875e",
58
- "03737d2862224ac6a10ed601d053cce5"
59
- ]
60
- FRED_KEYS: List[str] = ["faa11c8e2e4beee08c5b966e8b63a513"]
61
-
62
- CACHE: Dict[str, Dict[str, Any]] = {}
63
- CACHE_TIMES: Dict[str, float] = {}
64
- FEATURES_STORE: Dict[str, Dict[str, Any]] = {}
65
- MT5_MAX_AGE_SEC: int = 300
66
-
67
- twelve_counter: int = 0
68
- fred_counter: int = 0
69
- api_lock = threading.Lock()
70
-
71
- def get_next_twelve_key() -> str:
72
- global twelve_counter
73
- with api_lock:
74
- key = TWELVE_KEYS[twelve_counter % len(TWELVE_KEYS)]
75
- twelve_counter += 1
76
- return key
77
-
78
- def get_next_fred_key() -> str:
79
- global fred_counter
80
- with api_lock:
81
- key = FRED_KEYS[fred_counter % len(FRED_KEYS)]
82
- fred_counter += 1
83
- return key
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_21_gold_macro', 'symbol': SYMBOL, 'signal': signal_data.get('signal', {})},
90
- timeout=5
91
- )
92
- except:
93
- pass
94
 
95
- # ================= FRED ДАННЫЕ =================
96
- def fetch_fred_series(series_id: str, days: int = 30) -> List[Dict[str, Any]]:
97
- cache_key = f"fred_{series_id}_{days}"
98
- if cache_key in CACHE and time.time() - CACHE_TIMES.get(cache_key, 0) < 3600:
99
- return CACHE[cache_key]
100
 
 
 
101
  try:
102
- key = get_next_fred_key()
103
- url = f"https://api.stlouisfed.org/fred/series/observations?series_id={series_id}&api_key={key}&file_type=json&sort_order=desc&limit={days}"
104
- r = requests.get(url, timeout=10)
105
  if r.status_code == 200:
106
  data = r.json()
107
- values = [
108
- {'date': obs['date'], 'value': float(obs['value'])}
109
- for obs in data.get('observations', []) if obs['value'] != '.'
110
- ]
111
- CACHE[cache_key] = values
112
- CACHE_TIMES[cache_key] = time.time()
113
- return values
114
- except:
115
- pass
116
  return []
117
 
118
- def fetch_dxy() -> Dict[str, Any]:
119
- """Индекс доллара DXY."""
120
- values = fetch_fred_series("DTWEXBGS", 30)
121
  if len(values) >= 2:
122
  current = values[0]['value']
123
  month_ago = values[-1]['value']
@@ -126,225 +71,138 @@ def fetch_dxy() -> Dict[str, Any]:
126
  'dxy': round(current, 2),
127
  'change_1m': round(change, 2),
128
  'trend': 'STRENGTHENING' if change > 2 else 'WEAKENING' if change < -2 else 'STABLE',
129
- 'gold_signal': 'BEARISH' if change > 2 else 'BULLISH' if change < -2 else 'NEUTRAL'
130
  }
131
- return {'dxy': 104.5, 'trend': 'STABLE', 'gold_signal': 'NEUTRAL'}
132
 
133
- def fetch_tips_yield() -> Dict[str, Any]:
134
- """Реальная доходность TIPS (Treasury Inflation-Protected Securities)."""
135
- values = fetch_fred_series("DFII10", 30)
136
  if values:
137
  current = values[0]['value']
138
  return {
139
  'tips_yield': round(current, 4),
140
  'gold_signal': 'BULLISH' if current < 0 else 'BEARISH',
141
- 'analysis': 'Отрицательная реальная доходность = золото растёт' if current < 0 else 'Положительная доходность = давление на золото'
142
  }
143
  return {'tips_yield': 0.5, 'gold_signal': 'NEUTRAL'}
144
 
145
- # ================= ETF ПОТОКИ =================
146
- def fetch_etf_data(ticker: str) -> Optional[Dict[str, Any]]:
147
- cache_key = f"etf_{ticker}"
148
- if cache_key in CACHE and time.time() - CACHE_TIMES.get(cache_key, 0) < 900:
149
- return CACHE[cache_key]
150
-
151
  try:
152
- key = get_next_twelve_key()
153
- url = f"https://api.twelvedata.com/quote?symbol={ticker}&apikey={key}"
154
- r = requests.get(url, timeout=10)
155
  if r.status_code == 200:
156
  data = r.json()
157
- result = {
158
- 'ticker': ticker,
159
- 'price': float(data.get('close', 0)),
160
- 'change_pct': float(data.get('change_percent', 0)),
161
- 'volume': int(data.get('volume', 0)),
162
- 'signal': 'INFLOW' if float(data.get('change_percent', 0)) > 0 else 'OUTFLOW'
163
- }
164
- CACHE[cache_key] = result
165
- CACHE_TIMES[cache_key] = time.time()
166
- return result
167
  except:
168
  pass
169
  return None
170
 
171
- def analyze_etf_flows() -> Dict[str, Any]:
172
- etf_data: Dict[str, Any] = {}
173
- inflow_count = 0
174
- outflow_count = 0
175
-
176
- for name, ticker in GOLD_ETFS.items():
177
- data = fetch_etf_data(ticker)
178
- if data:
179
- etf_data[name] = data
180
- if data['signal'] == 'INFLOW':
181
- inflow_count += 1
182
- else:
183
- outflow_count += 1
184
- else:
185
- etf_data[name] = {"error": "no_data"}
186
-
187
- total = inflow_count + outflow_count
188
- inflow_pct = (inflow_count / total * 100) if total > 0 else 50
189
-
190
- if inflow_pct >= 75:
191
- signal = "STRONG_INFLOW"
192
- gold_signal = "BULLISH"
193
- elif inflow_pct >= 50:
194
- signal = "MODERATE_INFLOW"
195
- gold_signal = "SLIGHTLY_BULLISH"
196
- elif inflow_pct >= 25:
197
- signal = "MODERATE_OUTFLOW"
198
- gold_signal = "SLIGHTLY_BEARISH"
 
 
 
 
199
  else:
200
- signal = "STRONG_OUTFLOW"
201
- gold_signal = "BEARISH"
202
 
203
  return {
204
- 'etfs': etf_data,
205
- 'inflow_count': inflow_count,
206
- 'outflow_count': outflow_count,
207
- 'inflow_pct': round(inflow_pct, 1),
208
- 'flow_signal': signal,
209
- 'gold_signal': gold_signal
210
  }
211
 
212
- # ================= COT REPORT =================
213
- def fetch_cot_report() -> Dict[str, Any]:
214
- cache_key = "cot_gold"
215
- if cache_key in CACHE and time.time() - CACHE_TIMES.get(cache_key, 0) < 86400:
216
- return CACHE[cache_key]
217
-
218
  try:
219
- url = "https://raw.githubusercontent.com/datasets/cftc-commitment-of-traders/main/data/gold.csv"
220
- df = pd.read_csv(url)
221
  if not df.empty:
222
- latest = df.iloc[-1]
223
- comm_long = latest.get('Commercial_Long', 0)
224
- comm_short = latest.get('Commercial_Short', 0)
225
- noncomm_long = latest.get('Noncommercial_Long', 0)
226
- noncomm_short = latest.get('Noncommercial_Short', 0)
227
-
228
- comm_net = comm_long - comm_short
229
- noncomm_net = noncomm_long - noncomm_short
230
-
231
- # Спекулянты в лонг + хеджеры сокращают шорт = бычий сигнал
232
- if noncomm_net > 0 and comm_net > 0:
233
  cot_signal = "BULLISH"
234
- elif noncomm_net < 0 and comm_net < 0:
 
 
235
  cot_signal = "BEARISH"
236
  else:
237
- cot_signal = "MIXED"
238
 
239
- result = {
240
- 'report_date': str(latest.get('Date', 'Unknown')),
241
- 'commercial_net': int(comm_net),
242
- 'noncommercial_net': int(noncomm_net),
243
  'cot_signal': cot_signal,
244
- 'gold_signal': 'BULLISH' if cot_signal == 'BULLISH' else 'BEARISH' if cot_signal == 'BEARISH' else 'NEUTRAL'
245
  }
246
- CACHE[cache_key] = result
247
- CACHE_TIMES[cache_key] = time.time()
248
- return result
249
- except:
250
- pass
251
-
252
  return {'cot_signal': 'NEUTRAL', 'gold_signal': 'NEUTRAL'}
253
 
254
- # ================= GPR (ГЕОПОЛИТИЧЕСКИЙ РИСК) =================
255
- def fetch_gpr() -> Dict[str, Any]:
256
- """Геополитический риск — качественная оценка."""
257
  try:
258
- # Используем новости как прокси GPR
259
- r = requests.get(
260
- "https://newsapi.org/v2/everything?q=geopolitical+war+sanctions&pageSize=10&apiKey=948c7816beea47baa23b054592472d0e",
261
- timeout=10
262
- )
263
  if r.status_code == 200:
264
  total = r.json().get('totalResults', 0)
265
- if total > 1000:
266
- level = "HIGH"
267
- gold_signal = "BULLISH"
268
- elif total > 500:
269
- level = "ELEVATED"
270
- gold_signal = "SLIGHTLY_BULLISH"
 
 
271
  else:
272
- level = "LOW"
273
- gold_signal = "NEUTRAL"
274
-
275
- return {
276
- 'gpr_level': level,
277
- 'mentions_24h': total,
278
- 'gold_signal': gold_signal
279
- }
280
  except:
281
  pass
282
-
283
  return {'gpr_level': 'LOW', 'gold_signal': 'NEUTRAL'}
284
 
285
- # ================= ЦЕНТРАЛЬНЫЕ БАНКИ =================
286
- def fetch_central_bank_demand() -> Dict[str, Any]:
287
- """Спрос на золото со стороны центробанков."""
288
- try:
289
- r = requests.get("https://api.coingecko.com/api/v3/coins/gold?community_data=true", timeout=10)
290
- if r.status_code == 200:
291
- data = r.json()
292
- community = data.get('community_data', {})
293
- # Используем упоминания как прокси интереса
294
- mentions = community.get('twitter_followers', 0)
295
- return {
296
- 'interest_level': 'HIGH' if mentions > 100000 else 'MODERATE',
297
- 'gold_signal': 'BULLISH'
298
- }
299
- except:
300
- pass
301
- return {'interest_level': 'MODERATE', 'gold_signal': 'NEUTRAL'}
302
-
303
- # ================= АНАЛИЗ ИЗ MT5 =================
304
- def analyze_from_mt5(mt5_features: Dict[str, Any]) -> Optional[Dict[str, Any]]:
305
- try:
306
- score = 50.0
307
-
308
- rsi = mt5_features.get('H1_rsi', 50)
309
- if isinstance(rsi, (int, float)):
310
- if rsi > 70:
311
- score -= 10
312
- elif rsi < 30:
313
- score += 10
314
-
315
- ema = mt5_features.get('H1_price_vs_ema_21', 0)
316
- if isinstance(ema, (int, float)):
317
- if ema > 1.0:
318
- score += 10
319
- elif ema < -1.0:
320
- score -= 10
321
-
322
- score = max(0, min(100, score))
323
-
324
- if score > 60:
325
- direction, confidence = "LONG", score / 100
326
- elif score < 40:
327
- direction, confidence = "SHORT", (100 - score) / 100
328
- else:
329
- direction, confidence = "WAIT", 0.0
330
-
331
- return {
332
- "direction": direction,
333
- "confidence": round(confidence, 4),
334
- "macro_score": score,
335
- "source": "MT5"
336
- }
337
- except:
338
- return None
339
-
340
- # ================= ГЛАВНЫЙ АНАЛИЗ =================
341
- def analyze_gold_macro() -> Dict[str, Any]:
342
- dxy = fetch_dxy()
343
- tips = fetch_tips_yield()
344
- etf = analyze_etf_flows()
345
- cot = fetch_cot_report()
346
- gpr = fetch_gpr()
347
- central_bank = fetch_central_bank_demand()
348
 
349
  signals = []
350
  score = 50.0
@@ -352,10 +210,10 @@ def analyze_gold_macro() -> Dict[str, Any]:
352
  # DXY
353
  if dxy.get('gold_signal') == 'BULLISH':
354
  signals.append({"factor": "DXY", "signal": "BULLISH", "reason": "Доллар слабеет"})
355
- score += 10
356
  elif dxy.get('gold_signal') == 'BEARISH':
357
  signals.append({"factor": "DXY", "signal": "BEARISH", "reason": "Доллар укрепляется"})
358
- score -= 10
359
 
360
  # TIPS
361
  if tips.get('gold_signal') == 'BULLISH':
@@ -363,34 +221,31 @@ def analyze_gold_macro() -> Dict[str, Any]:
363
  score += 15
364
  elif tips.get('gold_signal') == 'BEARISH':
365
  signals.append({"factor": "TIPS", "signal": "BEARISH", "reason": "Положительная реальная доходность"})
366
- score -= 10
367
 
368
  # ETF
369
- if etf.get('gold_signal') == 'BULLISH' or etf.get('gold_signal') == 'SLIGHTLY_BULLISH':
370
- signals.append({"factor": "ETF", "signal": "BULLISH", "reason": f"Приток в ETF ({etf.get('inflow_pct', 0)}% в плюсе)"})
 
371
  score += 15
372
- elif etf.get('gold_signal') == 'BEARISH' or etf.get('gold_signal') == 'SLIGHTLY_BEARISH':
373
- signals.append({"factor": "ETF", "signal": "BEARISH", "reason": f"Отток из ETF"})
374
  score -= 15
375
 
376
  # COT
377
- if cot.get('gold_signal') == 'BULLISH':
378
- signals.append({"factor": "COT", "signal": "BULLISH", "reason": "Крупные игроки в лонг"})
 
379
  score += 12
380
- elif cot.get('gold_signal') == 'BEARISH':
381
- signals.append({"factor": "COT", "signal": "BEARISH", "reason": "Крупные игроки в шорт"})
382
  score -= 12
383
 
384
  # GPR
385
  if gpr.get('gold_signal') == 'BULLISH':
386
- signals.append({"factor": "GPR", "signal": "BULLISH", "reason": f"Геополитический риск: {gpr.get('gpr_level')}"})
387
  score += 10
388
 
389
- # Центробанки
390
- if central_bank.get('gold_signal') == 'BULLISH':
391
- signals.append({"factor": "CENTRAL_BANKS", "signal": "BULLISH", "reason": "Спрос центробанков"})
392
- score += 5
393
-
394
  score = max(0, min(100, score))
395
 
396
  if score > 60:
@@ -410,45 +265,14 @@ def analyze_gold_macro() -> Dict[str, Any]:
410
  "tips": tips,
411
  "etf_flows": etf,
412
  "cot_report": cot,
413
- "geopolitical_risk": gpr,
414
- "central_bank_demand": central_bank
415
  }
416
  }
417
 
418
  # ================= ГЛАВНЫЙ СИГНАЛ =================
419
- def get_gold_macro_signal() -> Dict[str, Any]:
420
  start = time.time()
421
-
422
- # Проверка MT5
423
- if SYMBOL in FEATURES_STORE:
424
- fs = FEATURES_STORE[SYMBOL]
425
- age = time.time() - fs.get("timestamp", 0)
426
- if age < MT5_MAX_AGE_SEC:
427
- mt5_features = fs.get("features", {})
428
- if mt5_features:
429
- mt5_result = analyze_from_mt5(mt5_features)
430
- if mt5_result:
431
- result = {
432
- "space": "space_21_gold_macro",
433
- "timestamp": int(time.time()),
434
- "symbol": SYMBOL,
435
- "signal": {
436
- "direction": mt5_result["direction"],
437
- "confidence": mt5_result["confidence"]
438
- },
439
- "macro_analysis": {
440
- "score": mt5_result["macro_score"],
441
- "signals": [{"factor": "MT5", "signal": mt5_result["direction"], "reason": "Технический анализ"}]
442
- },
443
- "data_source": "MT5",
444
- "meta": {"latency_ms": int((time.time() - start) * 1000)}
445
- }
446
- send_to_arbiter(result)
447
- print(f"🥇 GOLD MACRO: {mt5_result['direction']} | Score={mt5_result['macro_score']} (MT5)")
448
- return result
449
-
450
- # Fallback
451
- analysis = analyze_gold_macro()
452
  latency = int((time.time() - start) * 1000)
453
 
454
  result = {
@@ -464,77 +288,67 @@ def get_gold_macro_signal() -> Dict[str, Any]:
464
  "signals": analysis['signals'],
465
  "metrics": analysis['metrics']
466
  },
467
- "data_source": "FRED+ETF+COT",
468
- "meta": {"latency_ms": latency}
469
  }
470
 
471
- send_to_arbiter(result)
472
- print(f"🥇 GOLD MACRO v1: {analysis['direction']} | Score={analysis['macro_score']} | ETF={analysis['metrics']['etf_flows'].get('flow_signal')} | COT={analysis['metrics']['cot_report'].get('cot_signal')}")
 
 
 
 
 
 
 
 
 
473
  return result
474
 
475
- # ================= KEEP-ALIVE =================
476
- def keep_alive():
477
- while True:
478
- time.sleep(840)
479
- try:
480
- requests.get("http://localhost:7860/health", timeout=5)
481
- except:
482
- pass
483
 
484
- threading.Thread(target=keep_alive, daemon=True).start()
 
 
485
 
486
- # ================= FASTAPI =================
487
- app = FastAPI(title="TOMIRIS SPACE 21 v1.0 — GOLD MACRO & FLOW ANALYZER")
 
488
 
489
  @app.get("/health")
490
- @app.head("/health")
491
  async def health():
492
- return {
493
- "space": "Space 21 - Gold Macro & Flow Analyzer v1.0",
494
- "status": "operational",
495
- "symbol": SYMBOL,
496
- "features": ["DXY", "TIPS Yield", "ETF Flows", "COT Report", "GPR", "Central Bank Demand"]
497
- }
498
 
499
  @app.get("/consilium")
500
  async def consilium():
501
- return get_gold_macro_signal()
502
 
503
  @app.get("/dxy")
504
  async def dxy():
505
- return fetch_dxy()
506
 
507
  @app.get("/tips")
508
  async def tips():
509
- return fetch_tips_yield()
510
 
511
  @app.get("/etf")
512
  async def etf():
513
- return analyze_etf_flows()
514
 
515
  @app.get("/cot")
516
  async def cot():
517
- return fetch_cot_report()
518
 
519
  @app.get("/gpr")
520
  async def gpr():
521
- return fetch_gpr()
522
 
523
  @app.get("/full")
524
  async def full():
525
- return analyze_gold_macro()
526
-
527
- @app.post("/features")
528
- async def receive_features(data: Dict[str, Any]):
529
- symbol = data.get("symbol", SYMBOL)
530
- FEATURES_STORE[symbol] = {
531
- "features": data.get("features", {}),
532
- "price": data.get("price", 0.0),
533
- "timestamp": time.time()
534
- }
535
- print(f"📥 MT5 {symbol}: {len(data.get('features', {}))} признаков")
536
- return {"status": "ok"}
537
 
538
- print(f"🚀 SPACE 21 v1.0 — GOLD MACRO & FLOW ANALYZER ЗАПУЩЕН!")
539
- print(f"🥇 Анализ: DXY | TIPS | ETF GLD/IAU/GDX | COT | GPR | Центробанки")
540
- print(f"✅ Готов к бою!")
 
1
  # ============================================
2
  # АВТО-УСТАНОВКА ПАКЕТОВ
3
  # ============================================
4
+ import subprocess, sys, importlib
 
 
5
 
6
  REQUIRED_PACKAGES = {
7
  'numpy': 'numpy',
8
  'pandas': 'pandas',
9
+ 'httpx': 'httpx',
10
+ 'fastapi': 'fastapi',
11
+ 'uvicorn': 'uvicorn',
12
  'requests': 'requests'
13
  }
14
 
 
21
  print(f"✅ {pip_name} установлен!")
22
 
23
  # ============================================
24
+ # 👑 TOMIRIS SPACE 21 v3.0 — GOLD MACRO & FLOW ENGINE (No MT5, Real AUM, COT Index)
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
+
35
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
36
+ logger = logging.getLogger("Space21_GoldMacro")
37
 
38
  # ================= КОНФИГУРАЦИЯ =================
39
+ SYMBOL = "XAU/USD"
40
+ HUB_URL = os.getenv("SPACE17_URL", "https://tomiris-ai-name5-5.hf.space")
41
+ ARBITER_URL = os.getenv("SPACE18_URL", "https://tomiris-ai-name6-6.hf.space")
42
+ TWELVE_DATA_KEY = os.getenv("TWELVE_DATA_KEY", "f33e660d8c1945d19e3c4ded72a2875e")
43
+ FRED_KEY = os.getenv("FRED_KEY", "faa11c8e2e4beee08c5b966e8b63a513")
44
+ NEWSAPI_KEY = os.getenv("NEWSAPI_KEY", "948c7816beea47baa23b054592472d0e")
 
 
 
45
 
46
+ GOLD_ETFS = ["GLD", "IAU", "GDX"] # только крупнейшие
47
+ CACHE_TTL = {"fred": 3600, "etf_aum": 600, "cot": 86400, "gpr": 900}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
+ # ================= HTTP КЛИЕНТ =================
50
+ http_client = httpx.AsyncClient(timeout=15.0)
 
 
 
51
 
52
+ # ================= ЗАГРУЗКА ДАННЫХ =================
53
+ async def fetch_fred_series(series_id: str, days: int = 30) -> List[Dict]:
54
  try:
55
+ r = await http_client.get(
56
+ f"https://api.stlouisfed.org/fred/series/observations?series_id={series_id}&api_key={FRED_KEY}&file_type=json&sort_order=desc&limit={days}")
 
57
  if r.status_code == 200:
58
  data = r.json()
59
+ return [{'date': obs['date'], 'value': float(obs['value'])} for obs in data.get('observations', []) if obs['value'] != '.']
60
+ except Exception as e:
61
+ logger.warning(f"FRED {series_id}: {e}")
 
 
 
 
 
 
62
  return []
63
 
64
+ async def fetch_dxy() -> Dict[str, Any]:
65
+ values = await fetch_fred_series("DTWEXBGS", 30)
 
66
  if len(values) >= 2:
67
  current = values[0]['value']
68
  month_ago = values[-1]['value']
 
71
  'dxy': round(current, 2),
72
  'change_1m': round(change, 2),
73
  'trend': 'STRENGTHENING' if change > 2 else 'WEAKENING' if change < -2 else 'STABLE',
74
+ 'gold_signal': 'BULLISH' if change < -2 else 'BEARISH' if change > 2 else 'NEUTRAL'
75
  }
76
+ return {'dxy': 104.5, 'gold_signal': 'NEUTRAL'}
77
 
78
+ async def fetch_tips_yield() -> Dict[str, Any]:
79
+ values = await fetch_fred_series("DFII10", 30)
 
80
  if values:
81
  current = values[0]['value']
82
  return {
83
  'tips_yield': round(current, 4),
84
  'gold_signal': 'BULLISH' if current < 0 else 'BEARISH',
85
+ 'analysis': 'Отрицательная реальная доходность золото растёт' if current < 0 else 'Положительная доходность давление на золото'
86
  }
87
  return {'tips_yield': 0.5, 'gold_signal': 'NEUTRAL'}
88
 
89
+ # ================= ETF ПОТОКИ (ЧЕРЕЗ AUM) =================
90
+ async def fetch_etf_aum(ticker: str) -> Optional[float]:
91
+ """Получает Total Assets Under Management через TwelveData statistics."""
 
 
 
92
  try:
93
+ r = await http_client.get(f"https://api.twelvedata.com/statistics?symbol={ticker}&apikey={TWELVE_DATA_KEY}")
 
 
94
  if r.status_code == 200:
95
  data = r.json()
96
+ # total_assets в поле statistics -> fundamental
97
+ aum = data.get("statistics", {}).get("fundamental", {}).get("total_assets", 0)
98
+ return float(aum)
 
 
 
 
 
 
 
99
  except:
100
  pass
101
  return None
102
 
103
+ async def analyze_etf_flows() -> Dict[str, Any]:
104
+ """Рассчитывает притоки/оттоки по изменению AUM за последние 24 часа (если данные есть)."""
105
+ etf_data = {}
106
+ total_change = 0.0
107
+ count = 0
108
+ for ticker in GOLD_ETFS:
109
+ # Запрашиваем текущий AUM
110
+ aum_now = await fetch_etf_aum(ticker)
111
+ if aum_now is None:
112
+ continue
113
+ # Сохраняем предыдущее значение в кэше
114
+ prev_key = f"aum_{ticker}_prev"
115
+ prev_aum = CACHE.get(prev_key, aum_now)
116
+ change = aum_now - prev_aum
117
+ change_pct = (change / prev_aum * 100) if prev_aum > 0 else 0
118
+ etf_data[ticker] = {
119
+ "aum": aum_now,
120
+ "change_24h": round(change_pct, 2),
121
+ "signal": "INFLOW" if change > 0 else "OUTFLOW"
122
+ }
123
+ total_change += change_pct
124
+ count += 1
125
+ # Кэшируем текущее значение для следующего сравнения
126
+ CACHE[prev_key] = aum_now
127
+
128
+ avg_change = total_change / count if count else 0
129
+ if avg_change > 1:
130
+ flow_signal, gold_signal = "STRONG_INFLOW", "BULLISH"
131
+ elif avg_change > 0:
132
+ flow_signal, gold_signal = "MODERATE_INFLOW", "SLIGHTLY_BULLISH"
133
+ elif avg_change < -1:
134
+ flow_signal, gold_signal = "STRONG_OUTFLOW", "BEARISH"
135
  else:
136
+ flow_signal, gold_signal = "NEUTRAL", "NEUTRAL"
 
137
 
138
  return {
139
+ "etfs": etf_data,
140
+ "avg_change_pct": round(avg_change, 2),
141
+ "flow_signal": flow_signal,
142
+ "gold_signal": gold_signal
 
 
143
  }
144
 
145
+ # ================= COT С ИНДЕКСОМ =================
146
+ async def fetch_cot_report() -> Dict[str, Any]:
 
 
 
 
147
  try:
148
+ df = pd.read_csv("https://raw.githubusercontent.com/datasets/cftc-commitment-of-traders/main/data/gold.csv")
 
149
  if not df.empty:
150
+ # Вычисляем нетто спекулянтов
151
+ noncomm_net = df['Noncommercial_Long'] - df['Noncommercial_Short']
152
+ # Текущее значение
153
+ latest_net = int(noncomm_net.iloc[-1])
154
+ # Процентиль за последние 3 года (около 156 недель)
155
+ lookback = min(156, len(noncomm_net))
156
+ recent = noncomm_net.iloc[-lookback:]
157
+ percentile = (recent < latest_net).mean() * 100
158
+ if percentile > 80:
159
+ cot_signal = "BULLISH_EXTREME"
160
+ elif percentile > 60:
161
  cot_signal = "BULLISH"
162
+ elif percentile < 20:
163
+ cot_signal = "BEARISH_EXTREME"
164
+ elif percentile < 40:
165
  cot_signal = "BEARISH"
166
  else:
167
+ cot_signal = "NEUTRAL"
168
 
169
+ return {
170
+ 'report_date': str(df['Date'].iloc[-1]),
171
+ 'noncommercial_net': latest_net,
172
+ 'percentile': round(percentile, 1),
173
  'cot_signal': cot_signal,
174
+ 'gold_signal': 'BULLISH' if 'BULLISH' in cot_signal else 'BEARISH' if 'BEARISH' in cot_signal else 'NEUTRAL'
175
  }
176
+ except Exception as e:
177
+ logger.warning(f"COT error: {e}")
 
 
 
 
178
  return {'cot_signal': 'NEUTRAL', 'gold_signal': 'NEUTRAL'}
179
 
180
+ # ================= GPR (УЛУЧШЕННЫЙ) =================
181
+ async def fetch_gpr() -> Dict[str, Any]:
 
182
  try:
183
+ r = await http_client.get(f"https://newsapi.org/v2/everything?q=geopolitical+war+sanctions&pageSize=5&apiKey={NEWSAPI_KEY}")
 
 
 
 
184
  if r.status_code == 200:
185
  total = r.json().get('totalResults', 0)
186
+ # Нормализуем относительно общего количества новостей (грубо)
187
+ r_all = await http_client.get(f"https://newsapi.org/v2/everything?q=all&pageSize=5&apiKey={NEWSAPI_KEY}")
188
+ total_all = r_all.json().get('totalResults', 1) if r_all.status_code == 200 else 1000
189
+ ratio = total / max(total_all, 1)
190
+ if ratio > 0.15:
191
+ level, gold_signal = "HIGH", "BULLISH"
192
+ elif ratio > 0.08:
193
+ level, gold_signal = "ELEVATED", "SLIGHTLY_BULLISH"
194
  else:
195
+ level, gold_signal = "LOW", "NEUTRAL"
196
+ return {'gpr_level': level, 'mentions_share': round(ratio, 4), 'gold_signal': gold_signal}
 
 
 
 
 
 
197
  except:
198
  pass
 
199
  return {'gpr_level': 'LOW', 'gold_signal': 'NEUTRAL'}
200
 
201
+ # ================= ГЛАВНЫЙ МАКРО‑АНАЛИЗ =================
202
+ async def analyze_gold_macro() -> Dict[str, Any]:
203
+ dxy, tips, etf, cot, gpr = await asyncio.gather(
204
+ fetch_dxy(), fetch_tips_yield(), analyze_etf_flows(), fetch_cot_report(), fetch_gpr()
205
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
 
207
  signals = []
208
  score = 50.0
 
210
  # DXY
211
  if dxy.get('gold_signal') == 'BULLISH':
212
  signals.append({"factor": "DXY", "signal": "BULLISH", "reason": "Доллар слабеет"})
213
+ score += 12
214
  elif dxy.get('gold_signal') == 'BEARISH':
215
  signals.append({"factor": "DXY", "signal": "BEARISH", "reason": "Доллар укрепляется"})
216
+ score -= 12
217
 
218
  # TIPS
219
  if tips.get('gold_signal') == 'BULLISH':
 
221
  score += 15
222
  elif tips.get('gold_signal') == 'BEARISH':
223
  signals.append({"factor": "TIPS", "signal": "BEARISH", "reason": "Положительная реальная доходность"})
224
+ score -= 12
225
 
226
  # ETF
227
+ etf_signal = etf.get('gold_signal')
228
+ if etf_signal == 'BULLISH':
229
+ signals.append({"factor": "ETF Flow", "signal": "BULLISH", "reason": f"Приток в ETF ({etf['avg_change_pct']:.2f}%)"})
230
  score += 15
231
+ elif etf_signal == 'BEARISH':
232
+ signals.append({"factor": "ETF Flow", "signal": "BEARISH", "reason": f"Отток из ETF ({etf['avg_change_pct']:.2f}%)"})
233
  score -= 15
234
 
235
  # COT
236
+ cot_signal = cot.get('gold_signal')
237
+ if cot_signal == 'BULLISH':
238
+ signals.append({"factor": "COT", "signal": "BULLISH", "reason": f"Спекулянты в лонг (процентиль {cot.get('percentile', 50)}%)"})
239
  score += 12
240
+ elif cot_signal == 'BEARISH':
241
+ signals.append({"factor": "COT", "signal": "BEARISH", "reason": f"Спекулянты в шорт (процентиль {cot.get('percentile', 50)}%)"})
242
  score -= 12
243
 
244
  # GPR
245
  if gpr.get('gold_signal') == 'BULLISH':
246
+ signals.append({"factor": "GPR", "signal": "BULLISH", "reason": f"Геополитический риск повышен ({gpr.get('gpr_level')})"})
247
  score += 10
248
 
 
 
 
 
 
249
  score = max(0, min(100, score))
250
 
251
  if score > 60:
 
265
  "tips": tips,
266
  "etf_flows": etf,
267
  "cot_report": cot,
268
+ "geopolitical_risk": gpr
 
269
  }
270
  }
271
 
272
  # ================= ГЛАВНЫЙ СИГНАЛ =================
273
+ async def get_gold_macro_signal() -> Dict[str, Any]:
274
  start = time.time()
275
+ analysis = await analyze_gold_macro()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
  latency = int((time.time() - start) * 1000)
277
 
278
  result = {
 
288
  "signals": analysis['signals'],
289
  "metrics": analysis['metrics']
290
  },
291
+ "latency_ms": latency
 
292
  }
293
 
294
+ # Отправка в Arbiter
295
+ try:
296
+ await http_client.post(f"{ARBITER_URL}/log_signal", json={
297
+ "space": "space_21_gold_macro",
298
+ "symbol": SYMBOL,
299
+ "signal": result["signal"]
300
+ })
301
+ except:
302
+ pass
303
+
304
+ logger.info(f"🥇 Gold Macro: {analysis['direction']} | Score={analysis['macro_score']} | ETF={analysis['metrics']['etf_flows'].get('flow_signal')} | COT={analysis['metrics']['cot_report'].get('cot_signal')}")
305
  return result
306
 
307
+ # ================= FASTAPI =================
308
+ app = FastAPI(title="Tomiris Space 21 v3.0 — Gold Macro & Flow Engine")
 
 
 
 
 
 
309
 
310
+ @app.on_event("startup")
311
+ async def startup():
312
+ pass
313
 
314
+ @app.on_event("shutdown")
315
+ async def shutdown():
316
+ await http_client.aclose()
317
 
318
  @app.get("/health")
 
319
  async def health():
320
+ return {"status": "operational", "version": "3.0", "async": True, "no_mt5": True}
 
 
 
 
 
321
 
322
  @app.get("/consilium")
323
  async def consilium():
324
+ return await get_gold_macro_signal()
325
 
326
  @app.get("/dxy")
327
  async def dxy():
328
+ return await fetch_dxy()
329
 
330
  @app.get("/tips")
331
  async def tips():
332
+ return await fetch_tips_yield()
333
 
334
  @app.get("/etf")
335
  async def etf():
336
+ return await analyze_etf_flows()
337
 
338
  @app.get("/cot")
339
  async def cot():
340
+ return await fetch_cot_report()
341
 
342
  @app.get("/gpr")
343
  async def gpr():
344
+ return await fetch_gpr()
345
 
346
  @app.get("/full")
347
  async def full():
348
+ return await analyze_gold_macro()
349
+
350
+ if __name__ == "__main__":
351
+ import uvicorn
352
+ uvicorn.run(app, host="0.0.0.0", port=7860)
 
 
 
 
 
 
 
353
 
354
+ print("🚀 SPACE 21 v3.0 — GOLD MACRO & FLOW ENGINE ЗАПУЩЕН!")