Riy777 commited on
Commit
c22273f
·
verified ·
1 Parent(s): 86504de

Update ml_engine/data_manager.py

Browse files
Files changed (1) hide show
  1. ml_engine/data_manager.py +92 -97
ml_engine/data_manager.py CHANGED
@@ -1,6 +1,6 @@
1
  # ============================================================
2
  # 📂 ml_engine/data_manager.py
3
- # (V42.0 - GEM-Architect: The Titan Mirror Filter)
4
  # ============================================================
5
 
6
  import asyncio
@@ -10,6 +10,7 @@ import logging
10
  import pandas as pd
11
  import numpy as np
12
  import pandas_ta as ta
 
13
  from typing import List, Dict, Any
14
 
15
  import ccxt.async_support as ccxt
@@ -28,9 +29,9 @@ logging.getLogger("ccxt").setLevel(logging.WARNING)
28
 
29
  class DataManager:
30
  """
31
- DataManager V42.0 (Titan Mirror Filter)
32
- - L1 Screening is now structurally aligned with L2 Models.
33
- - Filters out 'Broken Charts', 'Low Liquidity', and 'Untradable Volatility'.
34
  """
35
 
36
  def __init__(self, contracts_db, whale_monitor, r2_service=None):
@@ -47,13 +48,13 @@ class DataManager:
47
  self.http_client = None
48
  self.market_cache = {}
49
 
50
- # قائمة سوداء موسعة للعملات غير القابلة للتداول
51
  self.BLACKLIST_TOKENS = [
52
  'USDT', 'USDC', 'DAI', 'TUSD', 'BUSD', 'FDUSD', 'EUR', 'PAX',
53
- 'UP', 'DOWN', 'BEAR', 'BULL', '3S', '3L', 'USDD', 'USDP', 'HT', 'KCS'
54
  ]
55
 
56
- print(f"📦 [DataManager V42.0] Titan Mirror Filter Online.")
57
 
58
  async def initialize(self):
59
  print(" > [DataManager] Starting initialization...")
@@ -85,52 +86,60 @@ class DataManager:
85
  def get_contracts_db(self): return self.contracts_db
86
 
87
  # ==================================================================
88
- # 🛡️ Layer 1: The Titan Mirror Filter (Engineered for ML Alignment)
89
  # ==================================================================
90
  async def layer1_rapid_screening(self) -> List[Dict[str, Any]]:
91
  """
92
- الفلتر الجديد: يرفض العملات بناءً على "جودة الهيكل" وليس فقط "إشارة المؤشر".
93
- يجهز الساحة لنماذج Titan و Sniper.
94
  """
95
  current_regime = getattr(SystemLimits, "CURRENT_REGIME", "RANGE")
96
  min_score = getattr(SystemLimits, "L1_MIN_AFFINITY_SCORE", 15.0)
97
 
98
- print(f"🔍 [L1 Mirror] Scanning for High-Fidelity Structures (Regime: {current_regime})...")
99
 
100
- # 1. جلب Universe (تصفية أولية حسب الحجم)
101
- # رفعنا حد السيولة لضمان عدم تمرير عملات ميتة للنماذج
102
- min_vol = 1000000 if current_regime == "BEAR" else 5000000
103
- all_tickers = await self._fetch_universe_tickers(min_volume=min_vol)
 
 
104
 
105
  if not all_tickers:
106
  print("⚠️ [Layer 1] Universe fetch returned empty.")
107
  return []
108
 
109
- # 2. الجلب العميق (Deep Fetch) لعدد محدود وعالي الجودة
110
- # نركز الموارد على أفضل 50 عملة سيولة بدلاً من فحص العشوائيات
111
- top_candidates = all_tickers[:200]
112
- enriched_data = await self._batch_fetch_ta_data(top_candidates, timeframe='15m', limit=200) # نحتاج 200 للمتوسطات
 
 
 
 
113
 
114
  scored_candidates = []
115
 
116
  for item in enriched_data:
117
  df = item.get('df')
118
- if df is None or len(df) < 150: continue # شرط صارم لطول البيانات
119
 
120
- # 🔥 تطبيق فلتر المرآة (The Mirror Logic)
121
  structural_score, reasons = self._calculate_structural_score(df, item['symbol'], current_regime)
122
 
 
 
 
 
123
  item['l1_score'] = structural_score
124
  item['type'] = " | ".join(reasons)
125
 
126
- # الفلتر يقبل فقط العملات ذات الهيكلية السليمة
127
  if structural_score >= min_score:
128
  scored_candidates.append(item)
129
 
130
- # الترتيب حسب الجودة الهيكلية
131
  scored_candidates.sort(key=lambda x: x['l1_score'], reverse=True)
132
 
133
- print(f" -> Mirror Filter selected {len(scored_candidates)} prime candidates.")
134
 
135
  return [
136
  {
@@ -140,111 +149,81 @@ class DataManager:
140
  'type': c.get('type', 'Structural'),
141
  'l1_score': c.get('l1_score', 0)
142
  }
143
- for c in scored_candidates[:25] # تمرير الأفضل فقط للمعالجة الثقيلة
144
  ]
145
 
146
  # ==================================================================
147
- # 🧬 Structural Alignment Engine (The Core Upgrade)
148
  # ==================================================================
149
  def _calculate_structural_score(self, df: pd.DataFrame, symbol: str, regime: str) -> (float, List[str]):
150
- """
151
- يحسب درجة توافق العملة مع متطلبات نماذج التعلم الآلي.
152
- """
153
  score = 0.0
154
  tags = []
155
-
156
  try:
157
- # 1. Data Prep
158
  close = df['close']
159
  high = df['high']
160
  low = df['low']
161
  volume = df['volume']
162
  current_price = close.iloc[-1]
163
 
164
- # --- Check A: Liquidity Quality (Anti-Sniper Failure) ---
165
- # فحص "جودة الشموع": هل هناك الكثير من الشموع الصفرية أو الفجوات؟
166
  zero_vol_candles = (volume == 0).sum()
167
- if zero_vol_candles > 5: return -100.0, ["Illiquid"] # رفض فوري
168
 
169
  avg_vol = volume.rolling(20).mean().iloc[-1]
170
- if avg_vol * current_price < 5000: return -100.0, ["Thin Book"] # سيولة دقيقة ضعيفة
171
 
172
- # --- Check B: Trend Alignment (Titan EMA Feature) ---
173
- # Titan يعتمد بشكل كبير على المسافة من EMA
174
  ema200 = ta.ema(close, length=200)
175
  ema50 = ta.ema(close, length=50)
176
 
177
  if ema200 is not None and ema50 is not None:
178
  curr_ema200 = ema200.iloc[-1]
179
- curr_ema50 = ema50.iloc[-1]
180
-
181
- # في السوق الصاعد، نريد السعر فوق المتوسطات ولكن ليس بعيداً جداً (Overextended)
182
  dist_200 = (current_price - curr_ema200) / curr_ema200
183
 
184
  if regime == "BULL":
185
  if current_price > curr_ema200:
186
  score += 20
187
- if current_price > curr_ema50: score += 10
188
- if dist_200 < 0.15: score += 10 # ليس متضخماً جداً
189
- else: score -= 5 # متضخم قد ينعكس
190
- else:
191
- score -= 20 # تحت التريند العام
192
  elif regime == "BEAR":
193
- # في الهبوط نبحث عن الارتدادات المفرطة (Mean Reversion)
194
- if dist_200 < -0.20: score += 30 # Oversold heavily
195
- else: # RANGE
196
- if abs(dist_200) < 0.05: score += 20 # يتذبذب حول المتوسط
197
 
198
- # --- Check C: Volatility Structure (Titan BB Feature) ---
199
  bb = ta.bbands(close, length=20, std=2)
200
  if bb is not None:
201
- # نحتاج حساب العرض (Bandwidth) يدوياً لضمان الدقة
202
- upper = bb[bb.columns[0]] # عادة Lower في pandas_ta
203
- lower = bb[bb.columns[2]] # Upper
204
- # تصحيح الاندكس حسب المكتبة، أحيانا تختلف، لذا نستخدم الأسماء لو أمكن
205
- # لكن للسرعة سنحسبها يدوياً
206
- bb_width = (bb.iloc[:, 2] - bb.iloc[:, 0]) / bb.iloc[:, 1] # (Upper - Lower) / Middle
207
- current_width = bb_width.iloc[-1]
208
-
209
- # Titan يحب "الانفجار من السكون" (Squeeze Breakout)
210
- if current_width < 0.05: # نطاق ضيق جداً (Squeeze)
211
- score += 25
212
- tags.append("Squeeze")
213
- elif current_width > 0.15: # متقلب جداً
214
- score -= 10 # قد يكون فات الأوان
215
 
216
- # --- Check D: Momentum Integrity (RSI/ADX) ---
217
  rsi = ta.rsi(close, length=14).iloc[-1]
218
  adx = ta.adx(high, low, close, length=14)
219
  curr_adx = adx.iloc[-1, 0] if adx is not None else 0
220
 
221
- if 40 < rsi < 70:
222
- score += 15 # منطقة صحية للتريند
223
- elif rsi > 75 and regime == "BULL":
224
- score += 10 # زخم قوي
225
- elif rsi < 30 and regime == "BEAR":
226
- score += 10 # ذروة بيع
227
 
228
- if curr_adx > 25:
229
- score += 10 # يوجد تريند حقيقي
230
- tags.append("Trending")
231
 
232
- # --- Check E: Volume Flow (Sniper Confirmation) ---
233
  vol_sma = volume.rolling(20).mean().iloc[-1]
234
- if volume.iloc[-1] > vol_sma * 1.5:
235
- score += 15
236
- tags.append("Vol Spike")
237
-
238
- except Exception as e:
239
- # print(f"⚠️ [Struct calc error] {symbol}: {e}")
240
- return 0.0, ["Error"]
241
 
 
242
  return score, tags
243
 
244
  # ==================================================================
245
- # 🌍 Universe & Batch Fetch (Enhanced)
246
  # ==================================================================
247
- async def _fetch_universe_tickers(self, min_volume=100_000) -> List[Dict[str, Any]]:
248
  try:
249
  tickers = await self.exchange.fetch_tickers()
250
  candidates = []
@@ -252,34 +231,51 @@ class DataManager:
252
  for symbol, ticker in tickers.items():
253
  if not symbol.endswith('/USDT'): continue
254
 
255
- # تصفية العملات المحظورة
256
  base_currency = symbol.split('/')[0]
257
  if any(bad in base_currency for bad in self.BLACKLIST_TOKENS): continue
258
- if "3S" in base_currency or "3L" in base_currency: continue # الرموز ذات الرافعة
259
 
260
  vol = ticker.get('quoteVolume')
261
  if vol is None: vol = ticker.get('info', {}).get('volValue')
262
  if vol is None: vol = 0.0
263
  else: vol = float(vol)
264
 
 
265
  if vol < min_volume: continue
266
 
267
- # فلتر السبريد (Spread Filter) - مهم جداً للـ Sniper
268
  bid = float(ticker.get('bid', 0) or 0)
269
  ask = float(ticker.get('ask', 0) or 0)
270
  if bid > 0 and ask > 0:
271
  spread_pct = (ask - bid) / bid
272
- if spread_pct > 0.01: continue # رفض السبريد العالي (>1%)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
 
274
  candidates.append({
275
  'symbol': symbol,
276
  'quote_volume': vol,
277
  'current_price': float(ticker['last']) if ticker.get('last') else 0.0,
278
- 'change_24h': float(ticker.get('percentage', 0.0))
 
279
  })
280
 
281
- # الترتيب حسب السيولة
282
- candidates.sort(key=lambda x: x['quote_volume'], reverse=True)
 
283
  return candidates
284
 
285
  except Exception as e:
@@ -288,13 +284,12 @@ class DataManager:
288
 
289
  async def _batch_fetch_ta_data(self, candidates, timeframe='15m', limit=200):
290
  results = []
291
- chunk_size = 20 # زيادة حجم الدفعة قليلاً
292
  for i in range(0, len(candidates), chunk_size):
293
  chunk = candidates[i:i+chunk_size]
294
  tasks = [self._fetch_ohlcv_safe(c, timeframe, limit) for c in chunk]
295
  chunk_res = await asyncio.gather(*tasks)
296
  results.extend([r for r in chunk_res if r is not None])
297
- # تأخير بسيط جداً لتجنب حظر API
298
  await asyncio.sleep(0.05)
299
  return results
300
 
@@ -303,10 +298,10 @@ class DataManager:
303
  ohlcv = await self.exchange.fetch_ohlcv(candidate['symbol'], tf, limit=limit)
304
  if not ohlcv: return None
305
  df = pd.DataFrame(ohlcv, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
306
- df['close'] = df['close'].astype(float)
307
- df['high'] = df['high'].astype(float)
308
- df['low'] = df['low'].astype(float)
309
- df['volume'] = df['volume'].astype(float)
310
  candidate['df'] = df
311
  return candidate
312
  except: return None
 
1
  # ============================================================
2
  # 📂 ml_engine/data_manager.py
3
+ # (V43.0 - GEM-Architect: Hot-Flow Sort)
4
  # ============================================================
5
 
6
  import asyncio
 
10
  import pandas as pd
11
  import numpy as np
12
  import pandas_ta as ta
13
+ import math # ✅ مكتبة مهمة للمعادلة اللوغاريتمية
14
  from typing import List, Dict, Any
15
 
16
  import ccxt.async_support as ccxt
 
29
 
30
  class DataManager:
31
  """
32
+ DataManager V43.0 (Hot-Flow Sort)
33
+ - Replaced 'Raw Volume' sort with 'Volume-Weighted Momentum'.
34
+ - Prioritizes active mid-caps over stagnant giants (XRP, TRX, BNB).
35
  """
36
 
37
  def __init__(self, contracts_db, whale_monitor, r2_service=None):
 
48
  self.http_client = None
49
  self.market_cache = {}
50
 
51
+ # القائمة السوداء
52
  self.BLACKLIST_TOKENS = [
53
  'USDT', 'USDC', 'DAI', 'TUSD', 'BUSD', 'FDUSD', 'EUR', 'PAX',
54
+ 'UP', 'DOWN', 'BEAR', 'BULL', '3S', '3L', 'USDD', 'USDP', 'HT', 'KCS', 'WBTC'
55
  ]
56
 
57
+ print(f"📦 [DataManager V43.0] Hot-Flow Engine Online.")
58
 
59
  async def initialize(self):
60
  print(" > [DataManager] Starting initialization...")
 
86
  def get_contracts_db(self): return self.contracts_db
87
 
88
  # ==================================================================
89
+ # 🛡️ Layer 1: The Titan Mirror Filter (Hot-Flow Edition)
90
  # ==================================================================
91
  async def layer1_rapid_screening(self) -> List[Dict[str, Any]]:
92
  """
93
+ يقوم بفرز العملات بناءً على "السخونة" (Hotness) وليس الحجم فقط.
 
94
  """
95
  current_regime = getattr(SystemLimits, "CURRENT_REGIME", "RANGE")
96
  min_score = getattr(SystemLimits, "L1_MIN_AFFINITY_SCORE", 15.0)
97
 
98
+ print(f"🔍 [L1 Hot-Flow] Scanning for Active Momentum (Regime: {current_regime})...")
99
 
100
+ # 1. جلب Universe (تصفية أولية)
101
+ # خفضنا الحد الأدنى للحجم للسماح للعملات المتوسطة لساخنة" بالدخول
102
+ # في السابق كان الرقم مرتفعاً جداً مما يقتل الفرص
103
+ min_vol_floor = 1000000 if current_regime == "BEAR" else 5000000
104
+
105
+ all_tickers = await self._fetch_universe_tickers(min_volume=min_vol_floor)
106
 
107
  if not all_tickers:
108
  print("⚠️ [Layer 1] Universe fetch returned empty.")
109
  return []
110
 
111
+ # 2. الجلب العميق (Deep Fetch) لأكثر العملات سخونة
112
+ # نأخذ أفضل 150 عملة بناءً على معادلة (الحجم × الحركة)
113
+ scan_limit = 150
114
+ top_candidates = all_tickers[:scan_limit]
115
+
116
+ print(f" -> Deep scanning top {len(top_candidates)} active assets...")
117
+
118
+ enriched_data = await self._batch_fetch_ta_data(top_candidates, timeframe='15m', limit=200)
119
 
120
  scored_candidates = []
121
 
122
  for item in enriched_data:
123
  df = item.get('df')
124
+ if df is None or len(df) < 150: continue
125
 
126
+ # تطبيق فلتر المرآة الهيكلية
127
  structural_score, reasons = self._calculate_structural_score(df, item['symbol'], current_regime)
128
 
129
+ # 🔥 Bonus: إضافة نقاط إضافية للعملات الساخنة جداً
130
+ hot_score = item.get('hot_score', 0)
131
+ if hot_score > 50: structural_score += 5 # Boost for super active coins
132
+
133
  item['l1_score'] = structural_score
134
  item['type'] = " | ".join(reasons)
135
 
 
136
  if structural_score >= min_score:
137
  scored_candidates.append(item)
138
 
139
+ # الترتيب النهائي حسب جودة الهيكل
140
  scored_candidates.sort(key=lambda x: x['l1_score'], reverse=True)
141
 
142
+ print(f" -> Filter selected {len(scored_candidates)} candidates.")
143
 
144
  return [
145
  {
 
149
  'type': c.get('type', 'Structural'),
150
  'l1_score': c.get('l1_score', 0)
151
  }
152
+ for c in scored_candidates[:30]
153
  ]
154
 
155
  # ==================================================================
156
+ # 🧬 Structural Alignment Engine
157
  # ==================================================================
158
  def _calculate_structural_score(self, df: pd.DataFrame, symbol: str, regime: str) -> (float, List[str]):
159
+ # (نفس منطق الفلتر الهيكلي السابق - لم يتغير لأنه ممتاز)
 
 
160
  score = 0.0
161
  tags = []
 
162
  try:
 
163
  close = df['close']
164
  high = df['high']
165
  low = df['low']
166
  volume = df['volume']
167
  current_price = close.iloc[-1]
168
 
 
 
169
  zero_vol_candles = (volume == 0).sum()
170
+ if zero_vol_candles > 5: return -100.0, ["Illiquid"]
171
 
172
  avg_vol = volume.rolling(20).mean().iloc[-1]
173
+ if avg_vol * current_price < 5000: return -100.0, ["Thin Book"]
174
 
 
 
175
  ema200 = ta.ema(close, length=200)
176
  ema50 = ta.ema(close, length=50)
177
 
178
  if ema200 is not None and ema50 is not None:
179
  curr_ema200 = ema200.iloc[-1]
 
 
 
180
  dist_200 = (current_price - curr_ema200) / curr_ema200
181
 
182
  if regime == "BULL":
183
  if current_price > curr_ema200:
184
  score += 20
185
+ if current_price > ema50.iloc[-1]: score += 10
186
+ if dist_200 < 0.15: score += 10
187
+ else: score -= 5
188
+ else: score -= 20
 
189
  elif regime == "BEAR":
190
+ if dist_200 < -0.20: score += 30
191
+ else:
192
+ if abs(dist_200) < 0.05: score += 20
 
193
 
 
194
  bb = ta.bbands(close, length=20, std=2)
195
  if bb is not None:
196
+ # حساب الـ Bandwidth يدوياً للأمان
197
+ upper = bb[bb.columns[0]] # Lower band usually index 0 in pandas_ta default
198
+ lower = bb[bb.columns[2]] # Upper band
199
+ # نستخدم أسماء الأعمدة إذا أمكن للتأكد، ولكن للسرعة:
200
+ # pandas_ta returns: Lower, Mid, Upper, Bandwidth, Percent
201
+ # لنتأكد من الـ Bandwidth مباشرة
202
+ width_col = next((c for c in bb.columns if c.startswith('BBB')), None)
203
+ if width_col:
204
+ current_width = bb[width_col].iloc[-1] / 100.0 # pandas_ta returns pct
205
+ if current_width < 0.05: score += 25; tags.append("Squeeze")
206
+ elif current_width > 0.15: score -= 10
 
 
 
207
 
 
208
  rsi = ta.rsi(close, length=14).iloc[-1]
209
  adx = ta.adx(high, low, close, length=14)
210
  curr_adx = adx.iloc[-1, 0] if adx is not None else 0
211
 
212
+ if 40 < rsi < 70: score += 15
213
+ elif rsi > 75 and regime == "BULL": score += 10
 
 
 
 
214
 
215
+ if curr_adx > 25: score += 10; tags.append("Trending")
 
 
216
 
 
217
  vol_sma = volume.rolling(20).mean().iloc[-1]
218
+ if volume.iloc[-1] > vol_sma * 1.5: score += 15; tags.append("Vol Spike")
 
 
 
 
 
 
219
 
220
+ except Exception as e: return 0.0, ["Error"]
221
  return score, tags
222
 
223
  # ==================================================================
224
+ # 🌍 Universe & Batch Fetch (The Hot-Flow Logic)
225
  # ==================================================================
226
+ async def _fetch_universe_tickers(self, min_volume=300_000) -> List[Dict[str, Any]]:
227
  try:
228
  tickers = await self.exchange.fetch_tickers()
229
  candidates = []
 
231
  for symbol, ticker in tickers.items():
232
  if not symbol.endswith('/USDT'): continue
233
 
 
234
  base_currency = symbol.split('/')[0]
235
  if any(bad in base_currency for bad in self.BLACKLIST_TOKENS): continue
236
+ if "3S" in base_currency or "3L" in base_currency: continue
237
 
238
  vol = ticker.get('quoteVolume')
239
  if vol is None: vol = ticker.get('info', {}).get('volValue')
240
  if vol is None: vol = 0.0
241
  else: vol = float(vol)
242
 
243
+ # 1. فلتر الحد الأدنى المطلق (لإبعاد العملات الميتة)
244
  if vol < min_volume: continue
245
 
246
+ # 2. فلتر السبريد (للحماية)
247
  bid = float(ticker.get('bid', 0) or 0)
248
  ask = float(ticker.get('ask', 0) or 0)
249
  if bid > 0 and ask > 0:
250
  spread_pct = (ask - bid) / bid
251
+ if spread_pct > 0.015: continue # تساهلنا قليلاً (1.5%) للسماح بعملات الـ Meme النشطة
252
+
253
+ # 3. حساب درجة السخونة 🔥 (Hot Score)
254
+ # المعادلة: Log10(Volume) * (1 + Abs(Change%))
255
+ # هذا يعطي وزناً للحجم، لكن يضرب بقوة في التغير السعري
256
+ change_pct = float(ticker.get('percentage', 0.0))
257
+
258
+ # نستخدم Log10 لتقليص الفارق بين المليار والمليون
259
+ # Log10(1B) = 9, Log10(10M) = 7 (الفارق بسيط)
260
+ # بينما التغير السعري: 1% vs 10% (الفارق 10 أضعاف)
261
+ # هذا يجعل التغير السعري هو العامل الحاسم في الترتيب
262
+
263
+ log_vol = math.log10(vol + 1)
264
+ volatility_factor = abs(change_pct) + 1.0 # نضيف 1 لكي لا نضرب في صفر
265
+
266
+ hot_score = log_vol * volatility_factor
267
 
268
  candidates.append({
269
  'symbol': symbol,
270
  'quote_volume': vol,
271
  'current_price': float(ticker['last']) if ticker.get('last') else 0.0,
272
+ 'change_24h': change_pct,
273
+ 'hot_score': hot_score # الدرجة الجديدة
274
  })
275
 
276
+ # 🔥 الترتيب حسب درجة السخونة وليس الحجم المطلق
277
+ candidates.sort(key=lambda x: x['hot_score'], reverse=True)
278
+
279
  return candidates
280
 
281
  except Exception as e:
 
284
 
285
  async def _batch_fetch_ta_data(self, candidates, timeframe='15m', limit=200):
286
  results = []
287
+ chunk_size = 20
288
  for i in range(0, len(candidates), chunk_size):
289
  chunk = candidates[i:i+chunk_size]
290
  tasks = [self._fetch_ohlcv_safe(c, timeframe, limit) for c in chunk]
291
  chunk_res = await asyncio.gather(*tasks)
292
  results.extend([r for r in chunk_res if r is not None])
 
293
  await asyncio.sleep(0.05)
294
  return results
295
 
 
298
  ohlcv = await self.exchange.fetch_ohlcv(candidate['symbol'], tf, limit=limit)
299
  if not ohlcv: return None
300
  df = pd.DataFrame(ohlcv, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
301
+ # تحويل البيانات لأرقام
302
+ cols = ['open', 'high', 'low', 'close', 'volume']
303
+ df[cols] = df[cols].apply(pd.to_numeric, errors='coerce')
304
+
305
  candidate['df'] = df
306
  return candidate
307
  except: return None