Update app.py
Browse files
app.py
CHANGED
|
@@ -1,11 +1,12 @@
|
|
| 1 |
#!/usr/bin/env python3
|
|
|
|
| 2 |
"""
|
| 3 |
-
SMC AI BOT v9.9 - Hugging Face Deployment (
|
| 4 |
-
-
|
| 5 |
-
-
|
| 6 |
-
-
|
| 7 |
-
-
|
| 8 |
-
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
@app.route('/health')
|
| 27 |
def health():
|
| 28 |
-
return {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 38 |
-
print(f"π Chat ID: {'β
' if CHAT_ID else 'β
|
| 39 |
-
print(f"π Twelve Data: {'β
' if TWELVEDATA_API_KEY else 'β
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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':
|
| 48 |
-
'
|
| 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 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
else:
|
| 62 |
-
|
|
|
|
|
|
|
| 63 |
|
| 64 |
-
|
|
|
|
|
|
|
| 65 |
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
}
|
| 73 |
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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","β","
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 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 |
-
|
| 282 |
-
|
| 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 |
-
|
| 290 |
-
|
| 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
|
| 294 |
filters.append(("Structure","β" if struct_conflict else "β
" if structure!='ranging' else "β οΈ",f"{structure}"))
|
| 295 |
-
bos_conflict
|
| 296 |
filters.append(("BOS/CHoCH","β" if bos_conflict else "β
" if bos!='none' else "β οΈ",f"{bos}"))
|
| 297 |
-
zone_conflict
|
| 298 |
filters.append(("Zone","β" if zone_conflict else "β
",f"{prem} ({pos:.0f}%)"))
|
| 299 |
-
rsi_conflict
|
| 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
|
| 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
|
| 306 |
filters.append(("PreferredTF","β
" if tf_ok else "β οΈ","Yes" if tf_ok else "No"))
|
| 307 |
-
sl_m
|
| 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
|
| 311 |
filters.append(("R:R","β
" if rr>=1.2 else "β",f"1:{rr}"))
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 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 |
-
|
| 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
|
| 342 |
-
|
|
|
|
| 343 |
π {now.strftime('%H:%M')} UTC | {session}
|
| 344 |
π Regime: {regime_info.get('regime','?')}
|
| 345 |
-
|
| 346 |
βββββββββββββββββββββ"""
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
emoji = {'BUY':'π’','SELL':'π΄','HOLD':'βͺ'}
|
| 351 |
for tf_name in ['5min','30min','1h','2h','4h']:
|
| 352 |
-
result
|
| 353 |
-
if not result: continue
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 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 |
-
|
| 376 |
-
# ============================================================
|
| 377 |
-
|
| 378 |
-
def fetch_5min(outputsize=500):
|
| 379 |
-
if not TWELVEDATA_API_KEY: return None
|
| 380 |
try:
|
| 381 |
-
r
|
| 382 |
-
|
| 383 |
-
if r.status_code
|
| 384 |
-
|
| 385 |
-
if 'values' in
|
| 386 |
-
candles
|
| 387 |
-
|
| 388 |
-
|
| 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 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 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
|
| 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
|
| 413 |
-
|
| 414 |
-
# ============================================================
|
| 415 |
-
# KEEP-ALIVE
|
| 416 |
-
# ============================================================
|
| 417 |
|
| 418 |
def keep_alive():
|
|
|
|
| 419 |
while True:
|
| 420 |
time.sleep(240)
|
| 421 |
-
try: requests.get("
|
| 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 |
-
|
| 432 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 441 |
|
| 442 |
while True:
|
| 443 |
try:
|
| 444 |
-
now = datetime.now(timezone.utc)
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
if 'WEEKEND' in session:
|
| 448 |
-
time.sleep(300); continue
|
| 449 |
|
| 450 |
-
|
| 451 |
seconds_since = (now - last_fetch).total_seconds()
|
|
|
|
| 452 |
|
| 453 |
-
if seconds_since >=
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
if
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 469 |
|
| 470 |
-
|
| 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 |
-
|
| 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)))
|
|
|