kulusia commited on
Commit
60bc115
Β·
verified Β·
1 Parent(s): 14d5929

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +325 -226
app.py CHANGED
@@ -1,11 +1,12 @@
1
  #!/usr/bin/env python3
 
2
  """
3
- SMC AI BOT v9.9 - Hugging Face Deployment (COMPLETE)
4
- - All feature functions included
5
- - Full 31-criteria filter analysis
6
- - 5-timeframe SMC signals
7
- - Telegram alerts every 5 minutes
8
- - Keep-alive ping to prevent sleeping
9
  """
10
 
11
  import os, numpy as np, pandas as pd, warnings, requests, time, threading
@@ -21,11 +22,25 @@ app = Flask(__name__)
21
 
22
  @app.route('/')
23
  def home():
24
- return "πŸ€– SMC AI Bot v9.9 β€” Live on Hugging Face"
 
 
 
25
 
26
  @app.route('/health')
27
  def health():
28
- return {"status": "alive", "time": str(datetime.now(timezone.utc)), "models": len(models)}
 
 
 
 
 
 
 
 
 
 
 
29
 
30
  # ============================================================
31
  # LOAD SECRETS
@@ -34,9 +49,30 @@ BOT_TOKEN = os.environ.get('TELEGRAM_BOT_TOKEN', '')
34
  CHAT_ID = os.environ.get('TELEGRAM_CHAT_ID', '')
35
  TWELVEDATA_API_KEY = os.environ.get('TWELVEDATA_API_KEY', '')
36
 
37
- print(f"πŸ”‘ Telegram Bot: {'βœ…' if BOT_TOKEN else '❌ MISSING'}")
38
- print(f"πŸ”‘ Chat ID: {'βœ…' if CHAT_ID else '❌ MISSING'}")
39
- print(f"πŸ”‘ Twelve Data: {'βœ…' if TWELVEDATA_API_KEY else '❌ MISSING'}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
  # ============================================================
42
  # LOAD MODELS
@@ -44,34 +80,153 @@ print(f"πŸ”‘ Twelve Data: {'βœ…' if TWELVEDATA_API_KEY else '❌ MISSING'}")
44
  print("\nπŸ“¦ Loading models...")
45
  models = {}
46
  model_files = {
47
- '5min': 'model_5min_entry.pkl',
48
- '30min': 'model_30min_bias.pkl',
49
- '1h': 'model_1h_bias.pkl',
50
- '2h': 'model_2h_bias.pkl',
51
- '4h': 'model_4h_bias.pkl',
52
  }
53
-
54
  for name, path in model_files.items():
55
  if os.path.exists(path):
56
- try:
57
- models[name] = joblib.load(path)
58
- print(f" βœ… {name.upper()}")
59
- except Exception as e:
60
- print(f" ❌ {name.upper()}: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  else:
62
- print(f" ❌ {name.upper()}: File not found")
 
 
63
 
64
- print(f"\nβœ… {len(models)}/5 models loaded")
 
 
65
 
66
- TIMEFRAMES = {
67
- '5min': {'minutes': 5, 'fwd': 4},
68
- '30min': {'minutes': 30, 'fwd': 4},
69
- '1h': {'minutes': 60, 'fwd': 6},
70
- '2h': {'minutes': 120, 'fwd': 4},
71
- '4h': {'minutes': 240, 'fwd': 3},
72
- }
73
 
74
- TF_LABELS = {'5min':'🎯 5min','30min':'πŸ”’ 30min','1h':'πŸ”’ 1H','2h':'πŸ”’ 2H','4h':'πŸ”’ 4H'}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
76
  # ============================================================
77
  # ALL FEATURE FUNCTIONS
@@ -213,9 +368,8 @@ def extract_features_20(df_slice):
213
  consecutive_streak(c,5)/5,range_compression(h,l,10),price_acceleration(c)/10]
214
 
215
  # ============================================================
216
- # REGIME DETECTION
217
  # ============================================================
218
-
219
  class RegimeDetector:
220
  def detect(self, df):
221
  if len(df) < 100: return {'regime':'NORMAL','confidence':0.5,'parameters':{'atr_sl':1.5,'atr_tp':2.0,'min_conf':0.6,'max_risk':1.0,'preferred_tfs':['5min','30min','1h','2h','4h']}}
@@ -242,275 +396,220 @@ def get_current_session():
242
  elif 20 <= h < 22: return 'POST_NY'
243
  return 'UNKNOWN'
244
 
 
 
 
245
  # ============================================================
246
  # SIGNAL GENERATION
247
  # ============================================================
248
-
249
  def get_signal(model, df):
250
  if len(df) < 201: return None
251
  features = np.array([extract_features_20(df.iloc[-201:-1])])
252
  try:
253
- probs = model.predict_proba(features)[0]
254
- pred = np.argmax(probs)
255
  return {'action':{0:'SELL',1:'HOLD',2:'BUY'}[pred],'confidence':float(probs[pred]),
256
  'all_probs':{'SELL':float(probs[0]),'HOLD':float(probs[1]),'BUY':float(probs[2])}}
257
  except: return None
258
 
259
  def analyze_signal(model, df, tf_name, tf_minutes, forward_bars, regime_params):
260
  bias = get_signal(model, df)
261
- if bias is None: return None, [("MODEL","❌","Prediction failed")]
262
-
263
- h,l,c = df['High'].values,df['Low'].values,df['Close'].values; o = df['Open'].values
264
- price = round(c[-1],2); atr_val = calculate_atr(h[-14:],l[-14:],c[-14:]); rsi_val = calculate_rsi(c)
265
-
266
- fp_df = pd.DataFrame({'Open':o,'High':h,'Low':l,'Close':c})
267
- footprint = calculate_footprint(fp_df)
268
- sh,sl_sw = find_swing_points(h,l,5)
269
- structure,_ = analyze_structure_from_swings(sh,sl_sw)
270
- bos = detect_bos_from_swings(sh,sl_sw,structure,c[-1])
271
- fvgs = find_fair_value_gaps(h,l); fvg_count = len(fvgs)
272
- prem,pos = calculate_premium_discount(h,l,c)
273
- t = df.index[-1]; kz = get_kill_zone_score(t.hour,t.minute)
274
-
275
- filters = []
276
- probs_str = f"S:{bias['all_probs']['SELL']:.0%} H:{bias['all_probs']['HOLD']:.0%} B:{bias['all_probs']['BUY']:.0%}"
277
- filters.append(("Probs","ℹ️",probs_str))
278
-
279
- if bias['action'] == 'HOLD':
280
  filters.append(("HOLD","βšͺ",f"HOLD ({bias['confidence']:.0%})"))
281
- signal = {'timeframe':TF_LABELS.get(tf_name,tf_name),'action':'HOLD','confidence':bias['confidence'],
282
- 'price':price,'stop_loss':0,'take_profit':0,'rr_ratio':0,'entry_time':'','exit_time':'',
283
- 'rsi':round(rsi_val,1),'atr':round(atr_val,2),'structure':structure,'bos':bos,'fvg_count':fvg_count,
284
- 'zone':f"{prem} ({pos:.0f}%)",'footprint':round(footprint,3),'kill_zone':kz,
285
- 'pass_count':0,'fail_count':1,'warn_count':0,'filters':filters,'all_probs':bias['all_probs']}
286
- return signal, filters
287
-
288
  filters.append(("HOLD","βœ…",f"Signal={bias['action']}"))
289
- fp_status = "βœ…" if abs(footprint)>=0.1 else "❌"
290
- filters.append(("FP-Neutral",fp_status,f"FP={footprint:.3f}"))
291
- fp_conflict = (bias['action']=='BUY' and footprint<0.0) or (bias['action']=='SELL' and footprint>0.0)
292
  filters.append(("FP-Conflict","❌" if fp_conflict else "βœ…","Opposes" if fp_conflict else "Aligned"))
293
- struct_conflict = (bias['action']=='BUY' and structure=='bearish') or (bias['action']=='SELL' and structure=='bullish')
294
  filters.append(("Structure","❌" if struct_conflict else "βœ…" if structure!='ranging' else "⚠️",f"{structure}"))
295
- bos_conflict = (bias['action']=='BUY' and bos=='choch_bearish') or (bias['action']=='SELL' and bos=='choch_bullish')
296
  filters.append(("BOS/CHoCH","❌" if bos_conflict else "βœ…" if bos!='none' else "⚠️",f"{bos}"))
297
- zone_conflict = (bias['action']=='BUY' and prem=='premium') or (bias['action']=='SELL' and prem=='discount')
298
  filters.append(("Zone","❌" if zone_conflict else "βœ…",f"{prem} ({pos:.0f}%)"))
299
- rsi_conflict = (bias['action']=='BUY' and rsi_val>75) or (bias['action']=='SELL' and rsi_val<25)
300
  filters.append(("RSI","❌" if rsi_conflict else "βœ…",f"{rsi_val:.1f}"))
301
  filters.append(("KillZone","βœ…" if kz>=1 else "⚠️",f"KZ={kz}"))
302
- min_c = regime_params.get('min_conf',0.6)
303
- conf_ok = bias['confidence']>=min_c
304
  filters.append(("Confidence","βœ…" if conf_ok else "❌",f"{bias['confidence']:.0%} (min={min_c:.0%})"))
305
- tf_ok = tf_name in regime_params.get('preferred_tfs',[])
306
  filters.append(("PreferredTF","βœ…" if tf_ok else "⚠️","Yes" if tf_ok else "No"))
307
- sl_m = regime_params.get('atr_sl',1.5); tp_m = regime_params.get('atr_tp',2.0)
308
  if bias['action']=='BUY': sl=round(price-atr_val*sl_m,2); tp=round(price+atr_val*tp_m,2)
309
  else: sl=round(price+atr_val*sl_m,2); tp=round(price-atr_val*tp_m,2)
310
- rr = round(abs(tp-price)/abs(sl-price),2) if sl!=price else 0
311
  filters.append(("R:R","βœ…" if rr>=1.2 else "❌",f"1:{rr}"))
312
-
313
- pc = sum(1 for _,s,_ in filters if s=="βœ…")
314
- wc = sum(1 for _,s,_ in filters if s=="⚠️")
315
- fc = sum(1 for _,s,_ in filters if s=="❌")
316
-
317
- et = datetime.now(timezone.utc)+timedelta(minutes=1)
318
- xt = et+timedelta(minutes=tf_minutes*forward_bars)
319
-
320
- signal = {'timeframe':TF_LABELS.get(tf_name,tf_name),'action':bias['action'],'confidence':bias['confidence'],
321
- 'price':price,'stop_loss':sl,'take_profit':tp,'rr_ratio':rr,
322
- 'entry_time':et.strftime('%H:%M UTC'),'exit_time':xt.strftime('%H:%M UTC'),
323
- 'rsi':round(rsi_val,1),'atr':round(atr_val,2),'structure':structure,'bos':bos,'fvg_count':fvg_count,
324
- 'zone':f"{prem} ({pos:.0f}%)",'footprint':round(footprint,3),'kill_zone':kz,
325
- 'pass_count':pc,'fail_count':fc,'warn_count':wc,'filters':filters,'all_probs':bias['all_probs']}
326
- return signal, filters
327
-
328
- # ============================================================
329
- # TELEGRAM
330
- # ============================================================
331
 
332
  def send_telegram(msg):
333
  if not BOT_TOKEN or not CHAT_ID: return
334
- try:
335
- requests.post(f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage",
336
- data={'chat_id':CHAT_ID,'text':msg,'parse_mode':'HTML'}, timeout=10)
337
- except Exception as e:
338
- print(f"⚠️ Telegram: {e}")
339
 
340
  def build_message(all_signals, session, regime_info):
341
- now = datetime.now(timezone.utc)
342
- msg = f"""<b>πŸ€– SMC BOT v9.9</b>
 
343
  πŸ• {now.strftime('%H:%M')} UTC | {session}
344
  πŸ”„ Regime: {regime_info.get('regime','?')}
345
- πŸ›‘οΈ Filters: Active
346
  ━━━━━━━━━━━━━━━━━━━━━"""
347
-
348
- if not all_signals: return msg + "\n\nβšͺ No predictions"
349
-
350
- emoji = {'BUY':'🟒','SELL':'πŸ”΄','HOLD':'βšͺ'}
351
  for tf_name in ['5min','30min','1h','2h','4h']:
352
- result = all_signals.get(tf_name)
353
- if not result: continue
354
- signal, filters = result
355
- e = emoji.get(signal['action'],'🟑')
356
-
357
- if signal['action'] == 'HOLD': verdict = "βšͺ HOLD"
358
- elif signal['fail_count'] == 0: verdict = "βœ… PASS"
359
- else: verdict = f"❌ BLOCKED({signal['fail_count']})"
360
-
361
- msg += f"\n\n{e} <b>{signal['timeframe']}</b> β†’ {signal['action']} ({signal['confidence']:.0%}) β€” {verdict}"
362
- msg += f"\n Probs: {signal.get('all_probs',{}).get('SELL',0):.0%}S/{signal.get('all_probs',{}).get('HOLD',0):.0%}H/{signal.get('all_probs',{}).get('BUY',0):.0%}B"
363
- if signal['action'] != 'HOLD':
364
- msg += f"\n πŸ’° ${signal['price']} | SL:${signal['stop_loss']} | TP:${signal['take_profit']} | R:R 1:{signal['rr_ratio']}"
365
- msg += f"\n ─────────────────────────"
366
- for fn, fs, fd in filters:
367
- msg += f"\n {fs} {fn:<15s}: {fd}"
368
-
369
- actions = [all_signals[tf][0]['action'] for tf in all_signals if all_signals[tf]]
370
- buys = actions.count('BUY'); sells = actions.count('SELL'); holds = actions.count('HOLD')
371
- msg += f"\n\n━━━━━━━━━━━━━━━━━━━━━\nπŸ“Š BUY:{buys} SELL:{sells} HOLD:{holds}"
372
  return msg
373
 
374
- # ============================================================
375
- # DATA FETCH
376
- # ============================================================
377
-
378
- def fetch_5min(outputsize=500):
379
- if not TWELVEDATA_API_KEY: return None
380
  try:
381
- r = requests.get("https://api.twelvedata.com/time_series",
382
- params={'symbol':'XAU/USD','interval':'5min','outputsize':outputsize,'timezone':'UTC','apikey':TWELVEDATA_API_KEY}, timeout=15)
383
- if r.status_code == 200:
384
- data = r.json()
385
- if 'values' in data and len(data['values']) > 0:
386
- candles = [{'datetime':pd.to_datetime(b['datetime']).tz_localize('UTC'),
387
- 'Open':float(b['open']),'High':float(b['high']),
388
- 'Low':float(b['low']),'Close':float(b['close']),'Volume':0} for b in data['values']]
389
- df = pd.DataFrame(candles).set_index('datetime').sort_index()
390
- df = df[df.index.weekday<5]; df = df[~((df.index.weekday==4)&(df.index.hour>=22))]
391
  return df
392
  return None
393
  except: return None
394
 
395
  def generate_tfs(df5):
396
  if df5 is None or len(df5)==0: return {}
397
- data = {'5min':df5}
398
- df30 = df5.resample('30min',label='right',closed='right').agg({'Open':'first','High':'max','Low':'min','Close':'last','Volume':'sum'}).dropna()
399
- data['30min'] = df30
400
- df1h = df5.resample('1h',label='right',closed='right').agg({'Open':'first','High':'max','Low':'min','Close':'last','Volume':'sum'}).dropna()
401
- data['1h'] = df1h
402
- df2h = df1h.resample('2h',label='right',closed='right').agg({'Open':'first','High':'max','Low':'min','Close':'last','Volume':'sum'}).dropna()
403
- data['2h'] = df2h
404
- df4h = df1h.resample('4h',label='right',closed='right').agg({'Open':'first','High':'max','Low':'min','Close':'last','Volume':'sum'}).dropna()
405
- data['4h'] = df4h
406
- return data
407
 
408
  def candle_just_closed(tf_minutes):
409
- now = datetime.now(timezone.utc)
410
  if now.weekday()>=5: return False
411
  if now.weekday()==4 and now.hour>=22: return False
412
- return (now.minute%tf_minutes)*60+now.second < 45
413
-
414
- # ============================================================
415
- # KEEP-ALIVE
416
- # ============================================================
417
 
418
  def keep_alive():
 
419
  while True:
420
  time.sleep(240)
421
- try: requests.get("https://kulusia-trade.hf.space/health", timeout=5)
422
  except: pass
423
 
424
  # ============================================================
425
  # MAIN BOT LOOP
426
  # ============================================================
427
-
428
  def run_bot():
 
429
  print("\nπŸ”„ Bot starting...")
430
- if not models:
431
- print("❌ No models loaded β€” cannot start")
432
- return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
433
 
434
  rd = RegimeDetector()
435
  last_fetch = datetime.now(timezone.utc) - timedelta(minutes=99)
436
  last_regime = datetime.now(timezone.utc) - timedelta(minutes=10)
 
437
  signal_count = 0
438
- data = {}
439
 
440
- send_telegram(f"πŸ€– <b>SMC Bot v9.9 LIVE on HF!</b>\nπŸ“Š {len(models)} models\nπŸ” Monitoring XAU/USD")
 
 
441
 
442
  while True:
443
  try:
444
- now = datetime.now(timezone.utc)
445
- session = get_current_session()
446
-
447
- if 'WEEKEND' in session:
448
- time.sleep(300); continue
449
 
450
- any_fetched = False
451
  seconds_since = (now - last_fetch).total_seconds()
 
452
 
453
- if seconds_since >= 285 and candle_just_closed(5):
454
- print(f"⏰ {now.strftime('%H:%M')} β€” fetching...")
455
- nd = fetch_5min(500)
456
- if nd is not None and len(nd) > 0:
457
- if '5min' in data:
458
- new_bars = nd[~nd.index.isin(data['5min'].index)]
459
- if len(new_bars) > 0:
460
- data['5min'] = pd.concat([data['5min'], new_bars])
461
- data['5min'] = data['5min'][~data['5min'].index.duplicated(keep='last')]
462
- data['5min'].sort_index(inplace=True)
463
- print(f" βœ… +{len(new_bars)} bars | Total: {len(data['5min']):,}")
464
- last_fetch = now; any_fetched = True
465
- else:
466
- data['5min'] = nd
467
- last_fetch = now; any_fetched = True
468
- print(f" βœ… Initial: {len(nd):,} bars")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
469
 
470
- time_since_regime = (now - last_regime).total_seconds()
471
-
472
- if any_fetched or time_since_regime >= 300:
473
- if any_fetched and '5min' in data:
474
- data = generate_tfs(data['5min'])
475
-
476
- if '1h' in data and len(data['1h']) >= 100:
477
- ri = rd.detect(data['1h']); last_regime = now
478
- else:
479
- ri = {'regime':'NORMAL','confidence':0.5,'parameters':{'atr_sl':1.5,'atr_tp':2.0,'min_conf':0.6,'max_risk':1.0,'preferred_tfs':['5min','30min','1h','2h','4h']}}
480
-
481
- all_signals = {}
482
- for tf_name in TIMEFRAMES:
483
- if tf_name in models and tf_name in data and len(data[tf_name]) >= 201:
484
- sig, filters = analyze_signal(
485
- models[tf_name], data[tf_name], tf_name,
486
- TIMEFRAMES[tf_name]['minutes'], TIMEFRAMES[tf_name]['fwd'],
487
- ri.get('parameters',{})
488
- )
489
- if sig: all_signals[tf_name] = (sig, filters)
490
-
491
- if all_signals:
492
- signal_count += 1
493
- msg = build_message(all_signals, session, ri)
494
- send_telegram(msg)
495
- print(f"πŸ“ Signal #{signal_count} β†’ Telegram")
496
-
497
- for tf_name, (sig, _) in all_signals.items():
498
- status = "βœ…" if sig['fail_count']==0 else f"❌({sig['fail_count']})"
499
- print(f" {sig['timeframe']}: {sig['action']} {sig['confidence']:.0%} | {status}")
500
 
501
  time.sleep(10)
502
-
503
  except Exception as e:
504
- print(f"⚠️ Error: {e}")
505
- time.sleep(60)
506
 
507
- # ============================================================
508
- # START
509
- # ============================================================
510
  threading.Thread(target=keep_alive, daemon=True).start()
511
- print("πŸ’“ Keep-alive active")
512
  threading.Thread(target=run_bot, daemon=True).start()
513
 
514
  if __name__ == '__main__':
515
- port = int(os.environ.get('PORT', 7860))
516
- app.run(host='0.0.0.0', port=port)
 
1
  #!/usr/bin/env python3
2
+ # BUILD v4 - Smart CSV update from last bar - 2026-05-08
3
  """
4
+ SMC AI BOT v9.9 - Hugging Face Deployment (SMART CSV UPDATE)
5
+ - Checks where uploaded CSV ends (date/time)
6
+ - Fetches only missing bars from Twelve Data
7
+ - Updates CSV in Space with new data
8
+ - Saves all timeframes to disk
9
+ - Signal sent ONLY when new candle data arrives
10
  """
11
 
12
  import os, numpy as np, pandas as pd, warnings, requests, time, threading
 
22
 
23
  @app.route('/')
24
  def home():
25
+ bars = len(data.get('5min', []))
26
+ tfs = [tf for tf in ['5min','30min','1h','2h','4h'] if tf in data and len(data[tf]) >= 201]
27
+ csv_date = get_csv_last_date(CSV_5MIN_LIVE) if os.path.exists(CSV_5MIN_LIVE) else None
28
+ return f"πŸ€– SMC Bot v9.9 β€” {bars:,} bars | Active: {len(tfs)}/5 TFs | CSV ends: {csv_date} | API: {api_call_count}/{MAX_API_CALLS_PER_DAY}"
29
 
30
  @app.route('/health')
31
  def health():
32
+ return {
33
+ "status": "alive", "time": str(datetime.now(timezone.utc)),
34
+ "models": len(models), "active_tfs": len([tf for tf in ['5min','30min','1h','2h','4h'] if tf in data and len(data[tf]) >= 201]),
35
+ "api_calls_today": api_call_count, "csv_last_bar": str(get_csv_last_date(CSV_5MIN_LIVE)) if os.path.exists(CSV_5MIN_LIVE) else None,
36
+ }
37
+
38
+ @app.route('/force-save')
39
+ def force_save():
40
+ global data
41
+ if not data: return "❌ No data", 500
42
+ save_all_csvs()
43
+ return "πŸ’Ύ Saved:\n" + "\n".join([f"{tf}: {len(data[tf]):,} bars" for tf in data])
44
 
45
  # ============================================================
46
  # LOAD SECRETS
 
49
  CHAT_ID = os.environ.get('TELEGRAM_CHAT_ID', '')
50
  TWELVEDATA_API_KEY = os.environ.get('TWELVEDATA_API_KEY', '')
51
 
52
+ print(f"πŸ”‘ Telegram: {'βœ…' if BOT_TOKEN else '❌'}")
53
+ print(f"πŸ”‘ Chat ID: {'βœ…' if CHAT_ID else '❌'}")
54
+ print(f"πŸ”‘ Twelve Data: {'βœ…' if TWELVEDATA_API_KEY else '❌'}")
55
+
56
+ # ============================================================
57
+ # API CALL TRACKING
58
+ # ============================================================
59
+ api_call_count = 0
60
+ api_call_reset = datetime.now(timezone.utc).date()
61
+ MAX_API_CALLS_PER_DAY = 600
62
+
63
+ def track_api_call():
64
+ global api_call_count, api_call_reset
65
+ today = datetime.now(timezone.utc).date()
66
+ if today != api_call_reset:
67
+ api_call_count = 0; api_call_reset = today
68
+ api_call_count += 1
69
+
70
+ def can_call_api():
71
+ global api_call_count, api_call_reset
72
+ today = datetime.now(timezone.utc).date()
73
+ if today != api_call_reset:
74
+ api_call_count = 0; api_call_reset = today
75
+ return api_call_count < MAX_API_CALLS_PER_DAY
76
 
77
  # ============================================================
78
  # LOAD MODELS
 
80
  print("\nπŸ“¦ Loading models...")
81
  models = {}
82
  model_files = {
83
+ '5min': 'model_5min_entry.pkl', '30min': 'model_30min_bias.pkl',
84
+ '1h': 'model_1h_bias.pkl', '2h': 'model_2h_bias.pkl', '4h': 'model_4h_bias.pkl',
 
 
 
85
  }
 
86
  for name, path in model_files.items():
87
  if os.path.exists(path):
88
+ try: models[name] = joblib.load(path); print(f" βœ… {name.upper()}")
89
+ except Exception as e: print(f" ❌ {name.upper()}: {e}")
90
+ else: print(f" ❌ {name.upper()}: File not found")
91
+ print(f"\nβœ… {len(models)}/5 models loaded")
92
+
93
+ # ============================================================
94
+ # CSV PATHS
95
+ # ============================================================
96
+ CSV_5MIN_LIVE = 'XAUUSD_5MIN_LIVE_UPDATED.csv'
97
+ CSV_30MIN_LIVE = 'XAUUSD_30MIN_LIVE_UPDATED.csv'
98
+ CSV_1H_LIVE = 'XAUUSD_1H_LIVE_UPDATED.csv'
99
+ CSV_2H_LIVE = 'XAUUSD_2H_LIVE_UPDATED.csv'
100
+ CSV_4H_LIVE = 'XAUUSD_4H_LIVE_UPDATED.csv'
101
+
102
+ CSV_MAP = {'5min': CSV_5MIN_LIVE, '30min': CSV_30MIN_LIVE, '1h': CSV_1H_LIVE, '2h': CSV_2H_LIVE, '4h': CSV_4H_LIVE}
103
+ TIMEFRAMES = {'5min': {'minutes': 5, 'fwd': 4}, '30min': {'minutes': 30, 'fwd': 4}, '1h': {'minutes': 60, 'fwd': 6}, '2h': {'minutes': 120, 'fwd': 4}, '4h': {'minutes': 240, 'fwd': 3}}
104
+ TF_LABELS = {'5min':'🎯 5min','30min':'πŸ”’ 30min','1h':'πŸ”’ 1H','2h':'πŸ”’ 2H','4h':'πŸ”’ 4H'}
105
+ data = {}
106
+
107
+ # ============================================================
108
+ # SMART CSV FUNCTIONS
109
+ # ============================================================
110
+
111
+ def get_csv_last_date(filepath):
112
+ """Get the last timestamp from a CSV file (fast - reads only last few bytes)"""
113
+ if not os.path.exists(filepath): return None
114
+ try:
115
+ # Fast method: read last 500 bytes
116
+ with open(filepath, 'rb') as f:
117
+ f.seek(-500, 2)
118
+ last_bytes = f.read().decode('utf-8', errors='ignore')
119
+ lines = last_bytes.strip().split('\n')
120
+ for line in reversed(lines):
121
+ if ',' in line and not line.startswith('datetime'):
122
+ ts_str = line.split(',')[0]
123
+ try:
124
+ return pd.to_datetime(ts_str).tz_localize('UTC') if 'UTC' not in ts_str and '+' not in ts_str else pd.to_datetime(ts_str)
125
+ except: continue
126
+ # Fallback: read full file
127
+ df = pd.read_csv(filepath, parse_dates=['datetime'], nrows=5)
128
+ return pd.to_datetime(df['datetime'].iloc[-1])
129
+ except: return None
130
+
131
+ def update_csv_from_api(filepath):
132
+ """
133
+ Check where CSV ends, fetch missing bars from API, update CSV.
134
+ Returns number of new bars added.
135
+ """
136
+ if not TWELVEDATA_API_KEY: return 0
137
+ if not can_call_api(): return 0
138
+
139
+ last_bar = get_csv_last_date(filepath)
140
+ now = datetime.now(timezone.utc)
141
+
142
+ # If no CSV or it's empty, fetch 5000 bars
143
+ if last_bar is None:
144
+ print(f" πŸ“‘ No existing data β€” fetching 5000 bars...")
145
+ df_new = fetch_5min(5000)
146
+ if df_new is not None and len(df_new) > 0:
147
+ df_new.to_csv(filepath)
148
+ print(f" βœ… Created: {len(df_new):,} bars")
149
+ return len(df_new)
150
+ return 0
151
+
152
+ # Calculate how many bars are missing
153
+ gap = now - last_bar
154
+ gap_minutes = gap.total_seconds() / 60
155
+ bars_needed = int(gap_minutes / 5) + 10 # 10 extra for safety
156
+
157
+ if bars_needed <= 2:
158
+ print(f" βœ… CSV is current (last bar: {last_bar})")
159
+ return 0
160
+
161
+ bars_needed = min(bars_needed, 5000)
162
+ print(f" πŸ“‘ CSV ends: {last_bar} | Need ~{bars_needed} bars...")
163
+
164
+ df_new = fetch_5min(bars_needed)
165
+ if df_new is None or len(df_new) == 0:
166
+ return 0
167
+
168
+ # Filter to only bars after last_bar
169
+ df_new = df_new[df_new.index > last_bar]
170
+
171
+ if len(df_new) == 0:
172
+ print(f" βœ… Already current")
173
+ return 0
174
+
175
+ # Load existing CSV, merge, save
176
+ df_existing = load_csv_data(filepath)
177
+ if df_existing is not None and len(df_existing) > 0:
178
+ df_combined = pd.concat([df_existing, df_new])
179
+ df_combined = df_combined[~df_combined.index.duplicated(keep='last')]
180
+ df_combined.sort_index(inplace=True)
181
+ added = len(df_combined) - len(df_existing)
182
+ df_combined.to_csv(filepath)
183
+ print(f" βœ… +{added} bars | Total: {len(df_combined):,} | Last: {df_combined.index[-1]}")
184
+ return added
185
  else:
186
+ df_new.to_csv(filepath)
187
+ print(f" βœ… Created: {len(df_new):,} bars")
188
+ return len(df_new)
189
 
190
+ # ============================================================
191
+ # DATA PERSISTENCE
192
+ # ============================================================
193
 
194
+ def save_all_csvs():
195
+ global data
196
+ if not data: return
197
+ for tf_name, path in CSV_MAP.items():
198
+ if tf_name in data and data[tf_name] is not None and len(data[tf_name]) > 0:
199
+ try: data[tf_name].to_csv(path)
200
+ except Exception as e: print(f" ⚠️ Save {tf_name}: {e}")
201
 
202
+ def load_csv_data(filepath):
203
+ if not os.path.exists(filepath): return None
204
+ try:
205
+ df = pd.read_csv(filepath, parse_dates=['datetime'])
206
+ if len(df) == 0: return None
207
+ df.set_index('datetime', inplace=True)
208
+ if df.index.tz is None: df.index = df.index.tz_localize('UTC')
209
+ rename = {}
210
+ for col in df.columns:
211
+ low = col.lower()
212
+ if low == 'open': rename[col] = 'Open'
213
+ elif low == 'high': rename[col] = 'High'
214
+ elif low == 'low': rename[col] = 'Low'
215
+ elif low == 'close': rename[col] = 'Close'
216
+ elif low == 'volume': rename[col] = 'Volume'
217
+ if rename: df.rename(columns=rename, inplace=True)
218
+ for col in ['Open','High','Low','Close']:
219
+ if col not in df.columns: return None
220
+ if 'Volume' not in df.columns: df['Volume'] = 0
221
+ df = df[df['Close'] > 0].dropna(subset=['Open','High','Low','Close'])
222
+ df = df[df['High'] >= df['Low']]
223
+ df = df[df.index.weekday < 5]
224
+ df = df[~((df.index.weekday == 4) & (df.index.hour >= 22))]
225
+ df.sort_index(inplace=True)
226
+ return df
227
+ except Exception as e:
228
+ print(f" ⚠️ Load {filepath}: {e}")
229
+ return None
230
 
231
  # ============================================================
232
  # ALL FEATURE FUNCTIONS
 
368
  consecutive_streak(c,5)/5,range_compression(h,l,10),price_acceleration(c)/10]
369
 
370
  # ============================================================
371
+ # REGIME + SESSION
372
  # ============================================================
 
373
  class RegimeDetector:
374
  def detect(self, df):
375
  if len(df) < 100: return {'regime':'NORMAL','confidence':0.5,'parameters':{'atr_sl':1.5,'atr_tp':2.0,'min_conf':0.6,'max_risk':1.0,'preferred_tfs':['5min','30min','1h','2h','4h']}}
 
396
  elif 20 <= h < 22: return 'POST_NY'
397
  return 'UNKNOWN'
398
 
399
+ def is_active_session(session):
400
+ return session in ['LONDON_KILLZONE','LONDON','NY_KILLZONE','LONDON_NY_OVERLAP','NY','ASIAN','PRE_LONDON']
401
+
402
  # ============================================================
403
  # SIGNAL GENERATION
404
  # ============================================================
 
405
  def get_signal(model, df):
406
  if len(df) < 201: return None
407
  features = np.array([extract_features_20(df.iloc[-201:-1])])
408
  try:
409
+ probs = model.predict_proba(features)[0]; pred = np.argmax(probs)
 
410
  return {'action':{0:'SELL',1:'HOLD',2:'BUY'}[pred],'confidence':float(probs[pred]),
411
  'all_probs':{'SELL':float(probs[0]),'HOLD':float(probs[1]),'BUY':float(probs[2])}}
412
  except: return None
413
 
414
  def analyze_signal(model, df, tf_name, tf_minutes, forward_bars, regime_params):
415
  bias = get_signal(model, df)
416
+ if bias is None: return None, [("MODEL","❌","Failed")]
417
+ h,l,c = df['High'].values,df['Low'].values,df['Close'].values; o=df['Open'].values
418
+ price=round(c[-1],2); atr_val=calculate_atr(h[-14:],l[-14:],c[-14:]); rsi_val=calculate_rsi(c)
419
+ fp_df=pd.DataFrame({'Open':o,'High':h,'Low':l,'Close':c}); footprint=calculate_footprint(fp_df)
420
+ sh,sl_sw=find_swing_points(h,l,5); structure,_=analyze_structure_from_swings(sh,sl_sw)
421
+ bos=detect_bos_from_swings(sh,sl_sw,structure,c[-1]); fvgs=find_fair_value_gaps(h,l)
422
+ prem,pos=calculate_premium_discount(h,l,c); kz=get_kill_zone_score(df.index[-1].hour,df.index[-1].minute)
423
+ filters=[("Probs","ℹ️",f"S:{bias['all_probs']['SELL']:.0%} H:{bias['all_probs']['HOLD']:.0%} B:{bias['all_probs']['BUY']:.0%}")]
424
+ if bias['action']=='HOLD':
 
 
 
 
 
 
 
 
 
 
425
  filters.append(("HOLD","βšͺ",f"HOLD ({bias['confidence']:.0%})"))
426
+ sig={'timeframe':TF_LABELS.get(tf_name,tf_name),'action':'HOLD','confidence':bias['confidence'],'price':price,'stop_loss':0,'take_profit':0,'rr_ratio':0,'entry_time':'','exit_time':'','rsi':round(rsi_val,1),'atr':round(atr_val,2),'structure':structure,'bos':bos,'fvg_count':len(fvgs),'zone':f"{prem} ({pos:.0f}%)",'footprint':round(footprint,3),'kill_zone':kz,'pass_count':0,'fail_count':1,'warn_count':0,'filters':filters,'all_probs':bias['all_probs']}
427
+ return sig, filters
 
 
 
 
 
428
  filters.append(("HOLD","βœ…",f"Signal={bias['action']}"))
429
+ filters.append(("FP-Neutral","βœ…" if abs(footprint)>=0.1 else "❌",f"FP={footprint:.3f}"))
430
+ fp_conflict=(bias['action']=='BUY' and footprint<0.0) or (bias['action']=='SELL' and footprint>0.0)
 
431
  filters.append(("FP-Conflict","❌" if fp_conflict else "βœ…","Opposes" if fp_conflict else "Aligned"))
432
+ struct_conflict=(bias['action']=='BUY' and structure=='bearish') or (bias['action']=='SELL' and structure=='bullish')
433
  filters.append(("Structure","❌" if struct_conflict else "βœ…" if structure!='ranging' else "⚠️",f"{structure}"))
434
+ bos_conflict=(bias['action']=='BUY' and bos=='choch_bearish') or (bias['action']=='SELL' and bos=='choch_bullish')
435
  filters.append(("BOS/CHoCH","❌" if bos_conflict else "βœ…" if bos!='none' else "⚠️",f"{bos}"))
436
+ zone_conflict=(bias['action']=='BUY' and prem=='premium') or (bias['action']=='SELL' and prem=='discount')
437
  filters.append(("Zone","❌" if zone_conflict else "βœ…",f"{prem} ({pos:.0f}%)"))
438
+ rsi_conflict=(bias['action']=='BUY' and rsi_val>75) or (bias['action']=='SELL' and rsi_val<25)
439
  filters.append(("RSI","❌" if rsi_conflict else "βœ…",f"{rsi_val:.1f}"))
440
  filters.append(("KillZone","βœ…" if kz>=1 else "⚠️",f"KZ={kz}"))
441
+ min_c=regime_params.get('min_conf',0.6); conf_ok=bias['confidence']>=min_c
 
442
  filters.append(("Confidence","βœ…" if conf_ok else "❌",f"{bias['confidence']:.0%} (min={min_c:.0%})"))
443
+ tf_ok=tf_name in regime_params.get('preferred_tfs',[])
444
  filters.append(("PreferredTF","βœ…" if tf_ok else "⚠️","Yes" if tf_ok else "No"))
445
+ sl_m=regime_params.get('atr_sl',1.5); tp_m=regime_params.get('atr_tp',2.0)
446
  if bias['action']=='BUY': sl=round(price-atr_val*sl_m,2); tp=round(price+atr_val*tp_m,2)
447
  else: sl=round(price+atr_val*sl_m,2); tp=round(price-atr_val*tp_m,2)
448
+ rr=round(abs(tp-price)/abs(sl-price),2) if sl!=price else 0
449
  filters.append(("R:R","βœ…" if rr>=1.2 else "❌",f"1:{rr}"))
450
+ pc=sum(1 for _,s,_ in filters if s=="βœ…"); wc=sum(1 for _,s,_ in filters if s=="⚠️"); fc=sum(1 for _,s,_ in filters if s=="❌")
451
+ et=datetime.now(timezone.utc)+timedelta(minutes=1); xt=et+timedelta(minutes=tf_minutes*forward_bars)
452
+ sig={'timeframe':TF_LABELS.get(tf_name,tf_name),'action':bias['action'],'confidence':bias['confidence'],'price':price,'stop_loss':sl,'take_profit':tp,'rr_ratio':rr,'entry_time':et.strftime('%H:%M UTC'),'exit_time':xt.strftime('%H:%M UTC'),'rsi':round(rsi_val,1),'atr':round(atr_val,2),'structure':structure,'bos':bos,'fvg_count':len(fvgs),'zone':f"{prem} ({pos:.0f}%)",'footprint':round(footprint,3),'kill_zone':kz,'pass_count':pc,'fail_count':fc,'warn_count':wc,'filters':filters,'all_probs':bias['all_probs']}
453
+ return sig, filters
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
454
 
455
  def send_telegram(msg):
456
  if not BOT_TOKEN or not CHAT_ID: return
457
+ try: requests.post(f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage", data={'chat_id':CHAT_ID,'text':msg,'parse_mode':'HTML'}, timeout=10)
458
+ except: pass
 
 
 
459
 
460
  def build_message(all_signals, session, regime_info):
461
+ now=datetime.now(timezone.utc)
462
+ active_tfs=len([tf for tf in ['5min','30min','1h','2h','4h'] if tf in data and len(data[tf])>=201])
463
+ msg=f"""<b>πŸ€– SMC BOT v9.9</b>
464
  πŸ• {now.strftime('%H:%M')} UTC | {session}
465
  πŸ”„ Regime: {regime_info.get('regime','?')}
466
+ πŸ“Š Active: {active_tfs}/5 | API: {api_call_count}/{MAX_API_CALLS_PER_DAY}
467
  ━━━━━━━━━━━━━━━━━━━━━"""
468
+ if not all_signals: return msg+"\n\nβšͺ No predictions"
469
+ emoji={'BUY':'🟒','SELL':'πŸ”΄','HOLD':'βšͺ'}
 
 
470
  for tf_name in ['5min','30min','1h','2h','4h']:
471
+ result=all_signals.get(tf_name)
472
+ if not result: msg+=f"\n\n⚠️ <b>{TF_LABELS.get(tf_name,tf_name)}</b>: Accumulating..."; continue
473
+ sig,filters=result; e=emoji.get(sig['action'],'🟑')
474
+ verdict="βšͺ HOLD" if sig['action']=='HOLD' else ("βœ… PASS" if sig['fail_count']==0 else f"❌ BLOCKED({sig['fail_count']})")
475
+ msg+=f"\n\n{e} <b>{sig['timeframe']}</b> β†’ {sig['action']} ({sig['confidence']:.0%}) β€” {verdict}"
476
+ msg+=f"\n Probs: {sig.get('all_probs',{}).get('SELL',0):.0%}S/{sig.get('all_probs',{}).get('HOLD',0):.0%}H/{sig.get('all_probs',{}).get('BUY',0):.0%}B"
477
+ if sig['action']!='HOLD': msg+=f"\n πŸ’° ${sig['price']} | SL:${sig['stop_loss']} | TP:${sig['take_profit']} | R:R 1:{sig['rr_ratio']}"
478
+ msg+="\n ─────────────────────────"
479
+ for fn,fs,fd in filters: msg+=f"\n {fs} {fn:<15s}: {fd}"
480
+ actions=[all_signals[tf][0]['action'] for tf in all_signals if all_signals[tf]]
481
+ buys=actions.count('BUY'); sells=actions.count('SELL'); holds=actions.count('HOLD')
482
+ msg+=f"\n\n━━━━━━━━━━━━━━━━━━━━━\nπŸ“Š BUY:{buys} SELL:{sells} HOLD:{holds}"
 
 
 
 
 
 
 
 
483
  return msg
484
 
485
+ def fetch_5min(outputsize=5000):
486
+ if not TWELVEDATA_API_KEY or not can_call_api(): return None
 
 
 
 
487
  try:
488
+ r=requests.get("https://api.twelvedata.com/time_series", params={'symbol':'XAU/USD','interval':'5min','outputsize':outputsize,'timezone':'UTC','apikey':TWELVEDATA_API_KEY}, timeout=15)
489
+ track_api_call()
490
+ if r.status_code==200:
491
+ d=r.json()
492
+ if 'values' in d and len(d['values'])>0:
493
+ candles=[{'datetime':pd.to_datetime(b['datetime']).tz_localize('UTC'),'Open':float(b['open']),'High':float(b['high']),'Low':float(b['low']),'Close':float(b['close']),'Volume':0} for b in d['values']]
494
+ df=pd.DataFrame(candles).set_index('datetime').sort_index()
495
+ df=df[df.index.weekday<5]; df=df[~((df.index.weekday==4)&(df.index.hour>=22))]
 
 
496
  return df
497
  return None
498
  except: return None
499
 
500
  def generate_tfs(df5):
501
  if df5 is None or len(df5)==0: return {}
502
+ result={'5min':df5}
503
+ result['30min']=df5.resample('30min',label='right',closed='right').agg({'Open':'first','High':'max','Low':'min','Close':'last','Volume':'sum'}).dropna()
504
+ result['1h']=df5.resample('1h',label='right',closed='right').agg({'Open':'first','High':'max','Low':'min','Close':'last','Volume':'sum'}).dropna()
505
+ result['2h']=result['1h'].resample('2h',label='right',closed='right').agg({'Open':'first','High':'max','Low':'min','Close':'last','Volume':'sum'}).dropna()
506
+ result['4h']=result['1h'].resample('4h',label='right',closed='right').agg({'Open':'first','High':'max','Low':'min','Close':'last','Volume':'sum'}).dropna()
507
+ return result
 
 
 
 
508
 
509
  def candle_just_closed(tf_minutes):
510
+ now=datetime.now(timezone.utc)
511
  if now.weekday()>=5: return False
512
  if now.weekday()==4 and now.hour>=22: return False
513
+ return (now.minute%tf_minutes)*60+now.second<45
 
 
 
 
514
 
515
  def keep_alive():
516
+ space_url=os.environ.get('SPACE_URL','https://kulusia-trade-bot.hf.space')
517
  while True:
518
  time.sleep(240)
519
+ try: requests.get(f"{space_url}/health",timeout=5)
520
  except: pass
521
 
522
  # ============================================================
523
  # MAIN BOT LOOP
524
  # ============================================================
 
525
  def run_bot():
526
+ global data, api_call_count
527
  print("\nπŸ”„ Bot starting...")
528
+ if not models: print("❌ No models"); return
529
+
530
+ # STEP 1: Check uploaded CSV and update it
531
+ print("\nπŸ“‚ Checking uploaded CSV...")
532
+ csv_exists = os.path.exists(CSV_5MIN_LIVE)
533
+ last_bar = get_csv_last_date(CSV_5MIN_LIVE)
534
+
535
+ if csv_exists:
536
+ print(f" βœ… CSV found: {last_bar}")
537
+ added = update_csv_from_api(CSV_5MIN_LIVE)
538
+ if added > 0:
539
+ print(f" πŸ“‘ Updated CSV with +{added} new bars")
540
+ else:
541
+ print(f" ⚠️ No CSV found β€” fetching fresh data...")
542
+
543
+ # STEP 2: Load data from CSV
544
+ data = {}
545
+ df5 = load_csv_data(CSV_5MIN_LIVE)
546
+ if df5 is not None and len(df5) > 0:
547
+ data = generate_tfs(df5)
548
+ save_all_csvs()
549
+ print(f" βœ… Loaded: 5min={len(data.get('5min',[])):,} bars")
550
 
551
  rd = RegimeDetector()
552
  last_fetch = datetime.now(timezone.utc) - timedelta(minutes=99)
553
  last_regime = datetime.now(timezone.utc) - timedelta(minutes=10)
554
+ last_save = datetime.now(timezone.utc)
555
  signal_count = 0
 
556
 
557
+ active_tfs = len([tf for tf in ['5min','30min','1h','2h','4h'] if tf in data and len(data[tf])>=201])
558
+ print(f"\nπŸ” Active: {active_tfs}/5 TFs\n")
559
+ send_telegram(f"πŸ€– <b>SMC Bot v9.9 LIVE!</b>\nπŸ“Š {len(models)} models | {active_tfs}/5 TFs\nπŸ“‚ CSV: {len(data.get('5min',[])):,} bars\nπŸ” Monitoring XAU/USD")
560
 
561
  while True:
562
  try:
563
+ now = datetime.now(timezone.utc); session = get_current_session()
564
+ if 'WEEKEND' in session: time.sleep(300); continue
 
 
 
565
 
566
+ active = is_active_session(session)
567
  seconds_since = (now - last_fetch).total_seconds()
568
+ fetch_interval = 285 if active else 885
569
 
570
+ if seconds_since >= fetch_interval and candle_just_closed(5 if active else 15):
571
+ if can_call_api():
572
+ print(f"⏰ {now.strftime('%H:%M')} β€” fetching...")
573
+ nd = fetch_5min(3)
574
+ if nd is not None and len(nd) > 0:
575
+ if '5min' in data:
576
+ new_bars = nd[~nd.index.isin(data['5min'].index)]
577
+ if len(new_bars) > 0:
578
+ data['5min'] = pd.concat([data['5min'], new_bars])
579
+ data['5min'] = data['5min'][~data['5min'].index.duplicated(keep='last')]
580
+ data['5min'].sort_index(inplace=True)
581
+ print(f" βœ… +{len(new_bars)} | Total: {len(data['5min']):,}")
582
+ last_fetch = now
583
+ data.update(generate_tfs(data['5min']))
584
+ save_all_csvs(); last_save = now
585
+
586
+ if '1h' in data and len(data['1h']) >= 100: ri = rd.detect(data['1h']); last_regime = now
587
+ else: ri = {'regime':'NORMAL','confidence':0.5,'parameters':{'atr_sl':1.5,'atr_tp':2.0,'min_conf':0.6,'max_risk':1.0,'preferred_tfs':['5min','30min','1h','2h','4h']}}
588
+
589
+ all_signals = {}
590
+ for tf_name in TIMEFRAMES:
591
+ if tf_name in models and tf_name in data and len(data[tf_name]) >= 201:
592
+ sig, filters = analyze_signal(models[tf_name], data[tf_name], tf_name, TIMEFRAMES[tf_name]['minutes'], TIMEFRAMES[tf_name]['fwd'], ri.get('parameters',{}))
593
+ if sig: all_signals[tf_name] = (sig, filters)
594
+
595
+ if all_signals:
596
+ signal_count += 1
597
+ send_telegram(build_message(all_signals, session, ri))
598
+ print(f"πŸ“ Signal #{signal_count} β†’ Telegram")
599
+ else:
600
+ data['5min'] = nd; last_fetch = now
601
+ data.update(generate_tfs(data['5min']))
602
+ save_all_csvs()
603
 
604
+ if (now - last_save).total_seconds() >= 3600:
605
+ save_all_csvs(); last_save = now
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
606
 
607
  time.sleep(10)
 
608
  except Exception as e:
609
+ print(f"⚠️ Error: {e}"); time.sleep(60)
 
610
 
 
 
 
611
  threading.Thread(target=keep_alive, daemon=True).start()
 
612
  threading.Thread(target=run_bot, daemon=True).start()
613
 
614
  if __name__ == '__main__':
615
+ app.run(host='0.0.0.0', port=int(os.environ.get('PORT', 7860)))