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

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +540 -0
app.py ADDED
@@ -0,0 +1,540 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
14
+ for module_name, pip_name in REQUIRED_PACKAGES.items():
15
+ try:
16
+ importlib.import_module(module_name)
17
+ except ImportError:
18
+ print(f"📦 Устанавливаю {pip_name}...")
19
+ subprocess.check_call([sys.executable, "-m", "pip", "install", pip_name])
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']
124
+ change = ((current - month_ago) / month_ago) * 100
125
+ return {
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
351
+
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':
362
+ signals.append({"factor": "TIPS", "signal": "BULLISH", "reason": "Отрицательная реальная доходность"})
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:
397
+ direction, confidence = "LONG", score / 100
398
+ elif score < 40:
399
+ direction, confidence = "SHORT", (100 - score) / 100
400
+ else:
401
+ direction, confidence = "WAIT", 0.0
402
+
403
+ return {
404
+ "macro_score": score,
405
+ "direction": direction,
406
+ "confidence": round(confidence, 4),
407
+ "signals": signals,
408
+ "metrics": {
409
+ "dxy": dxy,
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 = {
455
+ "space": "space_21_gold_macro",
456
+ "timestamp": int(time.time()),
457
+ "symbol": SYMBOL,
458
+ "signal": {
459
+ "direction": analysis['direction'],
460
+ "confidence": analysis['confidence']
461
+ },
462
+ "macro_analysis": {
463
+ "score": analysis['macro_score'],
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"✅ Готов к бою!")