Download bot.py from Monar1/bot: direct link, hf CLI and curl.
- Browser
- Download file 60.5 kB
-
https://huggingface.co/spaces/Monar1/bot/resolve/main/bot.py
- Command line
-
hf download hf://spaces/Monar1/bot/bot.py
-
curl -L -o bot.py https://huggingface.co/spaces/Monar1/bot/resolve/main/bot.py
60.5 kB
| import os | |
| import time | |
| import threading | |
| import requests as req | |
| import pandas as pd | |
| import yfinance as yf | |
| from datetime import datetime, timezone, timedelta, time as dtime | |
| from zoneinfo import ZoneInfo | |
| from flask import Flask | |
| BOT_TOKEN = os.environ["BOT_TOKEN"] | |
| CHAT_ID = os.environ["CHAT_ID"] | |
| SAUDI_TZ = timezone(timedelta(hours=3)) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # Telegram helpers | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| _tg = req.Session() | |
| def _post(endpoint, **kw): | |
| url = f"https://api.telegram.org/bot{BOT_TOKEN}/{endpoint}" | |
| for _ in range(3): | |
| try: | |
| return _tg.post(url, timeout=12, **kw).json() | |
| except Exception as e: | |
| print(f"TG {endpoint}: {e}") | |
| time.sleep(1) | |
| return {} | |
| def send(msg, cid=None): | |
| cid = cid or CHAT_ID | |
| for chunk in [msg[i:i+4000] for i in range(0, len(msg), 4000)]: | |
| _post("sendMessage", data={"chat_id": cid, "text": chunk}) | |
| def send_id(msg, cid=None): | |
| r = _post("sendMessage", data={"chat_id": cid or CHAT_ID, "text": msg}) | |
| return r.get("result", {}).get("message_id") | |
| def edit(cid, mid, text): | |
| _post("editMessageText", json={"chat_id": cid, "message_id": mid, "text": text}) | |
| def keyboard(msg, btns, cid=None): | |
| _post("sendMessage", json={"chat_id": cid or CHAT_ID, "text": msg, | |
| "reply_markup": {"inline_keyboard": btns}}) | |
| def answer_cb(cid): | |
| _post("answerCallbackQuery", data={"callback_query_id": cid}) | |
| def pbar(done, total, w=18): | |
| f = int(w * done / total) if total else 0 | |
| pct = int(100 * done / total) if total else 0 | |
| return f"[{'โ'*f}{'โ'*(w-f)}] {pct}%" | |
| def _vol_24h(df: pd.DataFrame) -> int: | |
| """Sum volume for last ~26 bars (one full 6.5-hour trading session).""" | |
| return int(df.tail(26)["Volume"].sum()) | |
| def _fmt_vol(v: int) -> str: | |
| if v >= 1_000_000_000: return f"{v/1_000_000_000:.1f}B" | |
| if v >= 1_000_000: return f"{v/1_000_000:.1f}M" | |
| if v >= 1_000: return f"{v/1_000:.0f}K" | |
| return str(v) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # yfinance data layer | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| BATCH = 50 # symbols per yfinance call | |
| VOL_MIN = 1_000_000 # minimum 24h volume to show a signal | |
| _MH_START = dtime(9, 30) | |
| _MH_END = dtime(16, 0) | |
| _PM_START = dtime(4, 0) | |
| _PM_END = dtime(9, 30) | |
| def _to_et(idx): | |
| """Convert DatetimeIndex to US/Eastern.""" | |
| if idx.tz is None: | |
| return idx.tz_localize("UTC").tz_convert("America/New_York") | |
| return idx.tz_convert("America/New_York") | |
| def _filter_mh(df: pd.DataFrame) -> pd.DataFrame: | |
| """Keep only regular market hours bars (9:30โ16:00 ET).""" | |
| et = _to_et(df.index) | |
| mask = (et.time >= _MH_START) & (et.time < _MH_END) | |
| out = df[mask].copy() | |
| out.index = et[mask] | |
| return out | |
| def _filter_pm(df: pd.DataFrame) -> pd.DataFrame: | |
| """Keep only pre-market bars (4:00โ9:30 ET).""" | |
| et = _to_et(df.index) | |
| mask = (et.time >= _PM_START) & (et.time < _PM_END) | |
| out = df[mask].copy() | |
| out.index = et[mask] | |
| return out | |
| def _extract_sym(raw, sym, multi): | |
| """Extract per-symbol DataFrame from a yf.download result.""" | |
| if multi: | |
| names = raw.columns.names | |
| if names[0] == "Ticker": | |
| if sym not in raw.columns.get_level_values(0): | |
| return None | |
| return raw[sym].copy() | |
| else: | |
| return raw.xs(sym, axis=1, level=1).copy() if sym in raw.columns.get_level_values(1) else None | |
| return raw.copy() | |
| def fetch_batch(symbols: list[str]) -> dict: | |
| """ | |
| Download 15-min candles for up to BATCH symbols at once. | |
| Returns {symbol: DataFrame(OHLCV, ET-indexed, market-hours only)}. | |
| """ | |
| results = {} | |
| if not symbols: | |
| return results | |
| try: | |
| raw = yf.download( | |
| symbols, | |
| period="5d", | |
| interval="15m", | |
| group_by="ticker", | |
| auto_adjust=True, | |
| progress=False, | |
| threads=True, | |
| ) | |
| if raw.empty: | |
| return results | |
| multi = isinstance(raw.columns, pd.MultiIndex) | |
| for sym in symbols: | |
| try: | |
| df = _extract_sym(raw, sym, multi) | |
| if df is None or (hasattr(df, "empty") and df.empty): | |
| continue | |
| df = _filter_mh(df) | |
| df = df.dropna(subset=["Open", "Close"]) | |
| if len(df) >= 2: | |
| results[sym] = df | |
| except Exception as ex: | |
| print(f"fetch_batch sym={sym}: {ex}") | |
| except Exception as e: | |
| print(f"fetch_batch error: {e}") | |
| return results | |
| def fetch_batch_pm(symbols: list[str]) -> dict: | |
| """ | |
| Download 15-min candles INCLUDING pre/post market for up to BATCH symbols. | |
| Returns {symbol: DataFrame(OHLCV, ET-indexed, pre-market only 4:00โ9:30)}. | |
| """ | |
| results = {} | |
| if not symbols: | |
| return results | |
| try: | |
| raw = yf.download( | |
| symbols, | |
| period="5d", | |
| interval="15m", | |
| group_by="ticker", | |
| auto_adjust=True, | |
| prepost=True, | |
| progress=False, | |
| threads=True, | |
| ) | |
| if raw.empty: | |
| return results | |
| multi = isinstance(raw.columns, pd.MultiIndex) | |
| for sym in symbols: | |
| try: | |
| df = _extract_sym(raw, sym, multi) | |
| if df is None or (hasattr(df, "empty") and df.empty): | |
| continue | |
| df = _filter_pm(df) | |
| df = df.dropna(subset=["Open", "Close"]) | |
| if len(df) >= 2: | |
| results[sym] = df | |
| except Exception as ex: | |
| print(f"fetch_batch_pm sym={sym}: {ex}") | |
| except Exception as e: | |
| print(f"fetch_batch_pm error: {e}") | |
| return results | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # Strategies | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def _bar_line(df, idx): | |
| row = df.iloc[idx] | |
| ts = df.index[idx].strftime("%m/%d %H:%M ET") | |
| pct = (row["Close"] - row["Open"]) / row["Open"] * 100 if row["Open"] else 0 | |
| return f"O:{row['Open']:.3f} C:{row['Close']:.3f} ({pct:+.1f}%) [{ts}]" | |
| # โโ Deduplication โ prevents re-alerting the same candle in auto scans โโโโโโโ | |
| _alerted_keys: set[str] = set() | |
| _alerted_date: str = "" | |
| def _is_new_signal(sym: str, candle_ts: str) -> bool: | |
| """Return True (and record) if this sym+candle hasn't been alerted today.""" | |
| global _alerted_keys, _alerted_date | |
| today = _now_et().strftime("%Y-%m-%d") | |
| if today != _alerted_date: | |
| _alerted_keys = set() | |
| _alerted_date = today | |
| key = f"{sym}@{candle_ts}" | |
| if key in _alerted_keys: | |
| return False | |
| _alerted_keys.add(key) | |
| return True | |
| def strat_pump_closed(sym, df, dedup: bool = False): | |
| """ | |
| Best 15-min candle of TODAY's session that moved โฅ15% openโclose. | |
| dedup=True (auto scans) โ skips already-alerted candles. | |
| dedup=False (manual) โ always shows. | |
| """ | |
| today = _now_et().date() | |
| closed = df.iloc[:-1] # exclude the live (unclosed) candle | |
| mask = [ts.date() == today for ts in closed.index] | |
| today_df = closed[mask] | |
| if today_df.empty: | |
| return None | |
| best_pct = 14.999 | |
| best_row = None | |
| best_ts = None | |
| for ts, row in today_df.iterrows(): | |
| if not row["Open"] or row["Open"] == 0: | |
| continue | |
| pct = (row["Close"] - row["Open"]) / row["Open"] * 100 | |
| if pct > best_pct: | |
| best_pct = pct | |
| best_row = row | |
| best_ts = ts | |
| if best_row is None: | |
| return None | |
| vol = _vol_24h(df) | |
| if vol < VOL_MIN: | |
| return None | |
| ts_str = best_ts.strftime("%H:%M ET") | |
| if dedup and not _is_new_signal(sym, ts_str): | |
| return None | |
| alert_time = datetime.now(SAUDI_TZ).strftime("%H:%M:%S KSA") | |
| return { | |
| "symbol": sym, "pct": best_pct, | |
| "msg": ( | |
| f"๐ข {sym} +{best_pct:.1f}%\n" | |
| f" ๐ต ุณุนุฑ ุงูุชูุจูู: {best_row['Close']:.3f}\n" | |
| f" ๐ O:{best_row['Open']:.3f} C:{best_row['Close']:.3f}" | |
| f" H:{best_row['High']:.3f} L:{best_row['Low']:.3f}\n" | |
| f" โฑ ุงูุดู ุนุฉ: {ts_str} | โฐ ููุช ุงูุชูุจูู: {alert_time}\n" | |
| f" ๐ง Vol 24H: {_fmt_vol(vol)}" | |
| ) | |
| } | |
| def strat_pump_live(sym, df, dedup: bool = False): | |
| """Current candle opened โฅ15% above previous close.""" | |
| prev, curr = df.iloc[-2], df.iloc[-1] | |
| if not prev["Close"] or prev["Close"] == 0: | |
| return None | |
| pct = (curr["Open"] - prev["Close"]) / prev["Close"] * 100 | |
| if pct < 15: | |
| return None | |
| vol = _vol_24h(df) | |
| if vol < VOL_MIN: | |
| return None | |
| ts_str = df.index[-1].strftime("%H:%M ET") | |
| if dedup and not _is_new_signal(sym, f"live@{ts_str}"): | |
| return None | |
| alert_time = datetime.now(SAUDI_TZ).strftime("%H:%M:%S KSA") | |
| return { | |
| "symbol": sym, "pct": pct, | |
| "msg": ( | |
| f"โก {sym} +{pct:.1f}%\n" | |
| f" ๐ต ุงูุณุนุฑ ุงูุญุงูู: {curr['Close']:.3f}\n" | |
| f" ๐ Prev C:{prev['Close']:.3f} โ Open:{curr['Open']:.3f}\n" | |
| f" โฑ ุงูุดู ุนุฉ: {ts_str} | โฐ ููุช ุงูุชูุจูู: {alert_time}\n" | |
| f" ๐ง Vol 24H: {_fmt_vol(vol)}" | |
| ) | |
| } | |
| def strat_pump_pm(sym, df, dedup: bool = False): | |
| """ | |
| Best 15-min pre-market candle of TODAY that moved โฅ10% openโclose. | |
| Lower threshold (10%) because pre-market moves differently. | |
| """ | |
| today = _now_et().date() | |
| mask = [ts.date() == today for ts in df.index] | |
| today_df = df[mask] | |
| if today_df.empty: | |
| return None | |
| best_pct = 9.999 | |
| best_row = None | |
| best_ts = None | |
| for ts, row in today_df.iterrows(): | |
| if not row["Open"] or row["Open"] == 0: | |
| continue | |
| pct = (row["Close"] - row["Open"]) / row["Open"] * 100 | |
| if pct > best_pct: | |
| best_pct = pct | |
| best_row = row | |
| best_ts = ts | |
| if best_row is None: | |
| return None | |
| vol = _vol_24h(df) | |
| if vol < VOL_MIN: | |
| return None | |
| ts_str = best_ts.strftime("%H:%M ET") | |
| if dedup and not _is_new_signal(sym, f"pm@{ts_str}"): | |
| return None | |
| alert_time = datetime.now(SAUDI_TZ).strftime("%H:%M:%S KSA") | |
| return { | |
| "symbol": sym, "pct": best_pct, | |
| "msg": ( | |
| f"๐ {sym} +{best_pct:.1f}%\n" | |
| f" ๐ต ุณุนุฑ Pre-market: {best_row['Close']:.3f}\n" | |
| f" ๐ O:{best_row['Open']:.3f} C:{best_row['Close']:.3f}" | |
| f" H:{best_row['High']:.3f} L:{best_row['Low']:.3f}\n" | |
| f" โฑ ุงูุดู ุนุฉ: {ts_str} PM | โฐ ููุช ุงูุชูุจูู: {alert_time}\n" | |
| f" ๐ง Vol PM: {_fmt_vol(vol)}" | |
| ) | |
| } | |
| # (strat_fn, stocks_name, label, is_premarket) | |
| STRATEGIES = { | |
| "pump_sc": (strat_pump_closed, "pump_stocks", "๐ PUMP Closed 15M โ Small Caps", False), | |
| "live_sc": (strat_pump_live, "pump_stocks", "โก PUMP LIVE 15M โ Small Caps", False), | |
| "pump_sp": (strat_pump_closed, "sp500_stocks", "๐ PUMP Closed 15M โ S&P 500", False), | |
| "live_sp": (strat_pump_live, "sp500_stocks", "โก PUMP LIVE 15M โ S&P 500", False), | |
| "pm_sc": (strat_pump_pm, "pump_stocks", "๐ Pre-market PUMP 15M โ Small Caps", True), | |
| "pm_sp": (strat_pump_pm, "sp500_stocks", "๐ Pre-market PUMP 15M โ S&P 500", True), | |
| } | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # /test command โ single stock inspection | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def cmd_test(sym, cid): | |
| sym = sym.upper().strip() | |
| send(f"๐ฌ ุฌุงุฑู ุฌูุจ {sym}โฆ", cid) | |
| t0 = time.time() | |
| data = fetch_batch([sym]) | |
| elapsed = time.time() - t0 | |
| if sym not in data: | |
| send(f"โ ูุง ุชูุฌุฏ ุจูุงูุงุช ูู {sym}\n" | |
| f"(ูุฏ ูููู ุงูุฑู ุฒ ุฎุงุทุฆุงู ุฃู ุงูุณูู ู ุบูู ุฃู ุงูุณูู ู ูุณุฑููุญ)\n" | |
| f"ููุช ุงูุฌูุจ: {elapsed:.1f}ุซ", cid) | |
| return | |
| df = data[sym] | |
| lines = [f"๐ {sym} โ {len(df)} ุดู ุนุฉ 15ุฏ (market hours)\n" | |
| f"ููุช ุงูุฌูุจ: {elapsed:.1f}ุซ\n{'โ'*32}"] | |
| for i in range(-min(4, len(df)), 0): | |
| row = df.iloc[i] | |
| ts = df.index[i].strftime("%m/%d %H:%M ET") | |
| pct = (row["Close"] - row["Open"]) / row["Open"] * 100 if row["Open"] else 0 | |
| arrow = "๐ข" if pct >= 0 else "๐ด" | |
| lines.append( | |
| f"{arrow} {ts}\n" | |
| f" O: {row['Open']:.4f} C: {row['Close']:.4f}\n" | |
| f" H: {row['High']:.4f} L: {row['Low']:.4f}\n" | |
| f" Chg: {pct:+.2f}% Vol: {int(row['Volume']):,}" | |
| ) | |
| lines.append("โ" * 32) | |
| if len(df) >= 2: | |
| pct_c = (df.iloc[-2]["Close"] - df.iloc[-2]["Open"]) / df.iloc[-2]["Open"] * 100 | |
| pct_g = (df.iloc[-1]["Open"] - df.iloc[-2]["Close"]) / df.iloc[-2]["Close"] * 100 | |
| lines.append(f"๐ ุขุฎุฑ ุดู ุนุฉ ู ุบููุฉ: {pct_c:+.2f}% {'โ โฅ15%' if pct_c >= 15 else 'โ <15%'}") | |
| lines.append(f"๐ Gap ุงูุดู ุนุฉ ุงูุญุงููุฉ: {pct_g:+.2f}% {'โ โฅ15%' if pct_g >= 15 else 'โ <15%'}") | |
| send("\n".join(lines), cid) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # Scan engine โ batch download, progress every batch | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| _scan_lock = threading.Lock() | |
| def run_scan_pm(strat_fn, stocks, label, cid): | |
| """Like run_scan but fetches pre-market data via fetch_batch_pm.""" | |
| if not _scan_lock.acquire(blocking=False): | |
| send("โ ๏ธ ูุญุต ุขุฎุฑ ูุนู ูุ ุงูุชุธุฑ ุญุชู ููุชูู.", cid) | |
| return | |
| try: | |
| total = len(stocks) | |
| found = [] | |
| total_ok = 0 | |
| total_fail = 0 | |
| start = time.time() | |
| batches = [stocks[i:i+BATCH] for i in range(0, total, BATCH)] | |
| nb = len(batches) | |
| msg_id = send_id( | |
| f"๐ Pre-market ูุญุต: {label}\n" | |
| f"{pbar(0, total)}\n" | |
| f"0/{total} | โ 0 ูุฌุญ | โ 0 ูุดู | ๐ฏ 0", | |
| cid | |
| ) | |
| for bi, batch in enumerate(batches): | |
| batch_data = fetch_batch_pm(batch) | |
| ok = len(batch_data) | |
| fail = len(batch) - ok | |
| total_ok += ok | |
| total_fail += fail | |
| for sym, df in batch_data.items(): | |
| result = strat_fn(sym, df, False) | |
| if result: | |
| found.append(result) | |
| done = min((bi + 1) * BATCH, total) | |
| elapsed = int(time.time() - start) | |
| txt = ( | |
| f"๐ ุฌุงุฑู: {label}\n" | |
| f"{pbar(done, total)}\n" | |
| f"{done}/{total} ุณูู | batch {bi+1}/{nb}\n" | |
| f"โ {total_ok} ูุฌุญ | โ {total_fail} ูุดู | ๐ฏ {len(found)}\n" | |
| f"โฑ {elapsed}ุซ ู ุถุช" | |
| ) | |
| if msg_id: | |
| edit(cid, msg_id, txt) | |
| elapsed = int(time.time() - start) | |
| stats_line = f"โ {total_ok} ูุฌุญ | โ {total_fail} ูุดู" | |
| if msg_id: | |
| edit(cid, msg_id, | |
| f"โ ุงูุชู ู: {label}\n" | |
| f"{pbar(total, total)}\n" | |
| f"{total}/{total} ุณูู | ๐ฏ {len(found)} ุฅุดุงุฑุฉ | โฑ {elapsed}ุซ\n" | |
| f"{stats_line}") | |
| now_s = datetime.now(SAUDI_TZ).strftime("%H:%M KSA") | |
| if found: | |
| found.sort(key=lambda x: x["pct"], reverse=True) | |
| hdr = ( | |
| f"๐ {label} โ {len(found)} ุฅุดุงุฑุฉ ({now_s})\n" | |
| f"{stats_line} | โฑ {elapsed}ุซ\n" | |
| f"{'โ'*30}\n" | |
| ) | |
| send(hdr + "\n\n".join(r["msg"] for r in found), cid) | |
| else: | |
| send(f"๐ ูุง ุฅุดุงุฑุงุช Pre-market โฅ10% โ {label} ({now_s})\n{stats_line} | โฑ {elapsed}ุซ", cid) | |
| return len(found) | |
| finally: | |
| _scan_lock.release() | |
| def run_scan(strat_fn, stocks, label, cid): | |
| if not _scan_lock.acquire(blocking=False): | |
| send("โ ๏ธ ูุญุต ุขุฎุฑ ูุนู ูุ ุงูุชุธุฑ ุญุชู ููุชูู.", cid) | |
| return | |
| try: | |
| total = len(stocks) | |
| found = [] | |
| total_ok = 0 | |
| total_fail = 0 | |
| start = time.time() | |
| batches = [stocks[i:i+BATCH] for i in range(0, total, BATCH)] | |
| nb = len(batches) | |
| msg_id = send_id( | |
| f"๐ ุจุฏุฃ ุงููุญุต: {label}\n" | |
| f"{pbar(0, total)}\n" | |
| f"0/{total} | โ 0 ูุฌุญ | โ 0 ูุดู | ๐ฏ 0", | |
| cid | |
| ) | |
| for bi, batch in enumerate(batches): | |
| batch_data = fetch_batch(batch) | |
| ok = len(batch_data) | |
| fail = len(batch) - ok | |
| total_ok += ok | |
| total_fail += fail | |
| for sym, df in batch_data.items(): | |
| result = strat_fn(sym, df, False) # manual scan โ no dedup | |
| if result: | |
| found.append(result) | |
| done = min((bi + 1) * BATCH, total) | |
| elapsed = int(time.time() - start) | |
| txt = ( | |
| f"๐ ุฌุงุฑู: {label}\n" | |
| f"{pbar(done, total)}\n" | |
| f"{done}/{total} ุณูู | batch {bi+1}/{nb}\n" | |
| f"โ {total_ok} ูุฌุญ | โ {total_fail} ูุดู | ๐ฏ {len(found)}\n" | |
| f"โฑ {elapsed}ุซ ู ุถุช" | |
| ) | |
| if msg_id: | |
| edit(cid, msg_id, txt) | |
| elapsed = int(time.time() - start) | |
| stats_line = f"โ {total_ok} ูุฌุญ | โ {total_fail} ูุดู" | |
| if msg_id: | |
| edit(cid, msg_id, | |
| f"โ ุงูุชู ู: {label}\n" | |
| f"{pbar(total, total)}\n" | |
| f"{total}/{total} ุณูู | ๐ฏ {len(found)} ุฅุดุงุฑุฉ | โฑ {elapsed}ุซ\n" | |
| f"{stats_line}") | |
| now_s = datetime.now(SAUDI_TZ).strftime("%H:%M KSA") | |
| if found: | |
| found.sort(key=lambda x: x["pct"], reverse=True) | |
| hdr = ( | |
| f"๐ฅ {label} โ {len(found)} ุฅุดุงุฑุฉ ({now_s})\n" | |
| f"{stats_line} | โฑ {elapsed}ุซ\n" | |
| f"{'โ'*30}\n" | |
| ) | |
| send(hdr + "\n\n".join(r["msg"] for r in found), cid) | |
| else: | |
| sample = list(fetch_batch([stocks[0]]).values()) | |
| smsg = ( | |
| f"๐ ูุง ุฅุดุงุฑุงุช โฅ15% โ {label} ({now_s})\n" | |
| f"{stats_line} | โฑ {elapsed}ุซ\n\n" | |
| ) | |
| if sample: | |
| df = sample[0] | |
| sym = stocks[0] | |
| smsg += ( | |
| f"๐ ุนููุฉ ุชุญูู ({sym}):\n" | |
| f" {_bar_line(df, -2)} โ ุขุฎุฑ ู ุบููุฉ\n" | |
| f" {_bar_line(df, -1)} โ ุงูุญุงููุฉ\n\n" | |
| f"โน๏ธ ุฌุฑุจ /test SYMBOL ููุชุญูู ู ู ุณูู ุจุนููู" | |
| ) | |
| send(smsg, cid) | |
| return len(found) | |
| finally: | |
| _scan_lock.release() | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # Stocks | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| sp500_stocks = [ | |
| "NVDA","AAPL","GOOGL","GOOG","MSFT","AMZN","AVGO","META","TSLA","LLY", | |
| "BRK-B","MU","WMT","JPM","AMD","XOM","ORCL","V","JNJ","INTC", | |
| "CSCO","MA","COST","CAT","ABBV","BAC","LRCX","CVX","ARM","UNH","AMAT", | |
| "NFLX","GE","KO","PG","MS","PLTR","HD","GS","MRK","PM","IBM","TXN", | |
| "DELL","KLAC","WFC","RTX","LIN","MRVL","QCOM","C","PANW", | |
| "AXP","MCD","ADI","ANET","PEP","TMUS","VZ","AMGN","APP","NEE", | |
| "TJX","TMO","DIS","CRWD","APH","BA","BLK","UNP","GILD","ABT", | |
| "T","DE","SCHW","ETN","GLW","CRM","ISRG","PFE","UBER","IBKR", | |
| "COP","SHOP","BX","PLD","HON","DHR","BKNG","CB","SPGI","CVS","PDD", | |
| "LMT","MO","PGR","LOW","SYK","BMY","NOW","VRT","VRTX","COF","PH", | |
| "ACN","SBUX","EQIX","NEM","FTNT","MDT","SO","PWR","CDNS","MAR","ADBE", | |
| "TT","HWM","DUK","GD","CME","MCK","ADP","UPS","CEG","PNC","FCX", | |
| "AMT","ELV","CMI","SNPS","WM","WMB","JCI","MNST","CSX","USB","CMCSA", | |
| "KKR","DDOG","HCA","SLB","MELI","INTU","ABNB","MMM","ICE","MDLZ", | |
| "FDX","MCO","HLT","EMR","NOC","CI","MPC","VLO","SHW","RCL","ORLY", | |
| "NXPI","HOOD","GM","ROST","APO","PSX","EOG","CVNA","MPWR","ITW", | |
| "ECL","BSX","CTAS","CL","KMI","NSC","AEP","CRH","AON","TDG", | |
| "DASH","SPG","MSI","URI","REGN","WBD","DLR","HPE","RSG", | |
| "TRV","NKE","APD","BKR","TEL","PCAR","GWW","TFC","AFL","SRE","F","D", | |
| "NUE","LHX","ALL","O","TRGP","OXY","KEYS","TER","CARR","TGT","OKE", | |
| "AJG","MET","PSA","FAST","COR","DAL","AME","CTVA","AZO","DVN", | |
| "EA","ETR","ODFL","VST","ROK","EW","NDAQ","XEL","EBAY","ADSK","CAH", | |
| "MCHP","FITB","EXC","ON","GRMN","MSCI","STT","IDXX","WAB","MSTR", | |
| "HUM","BDX","YUM","KDP","DHI","CCI","AMP","VTR","ALNY", | |
| "COIN","AIG","TTWO","PEG","KR","AXON","ED","ADM","STLD","CBRE", | |
| "CCL","PCG","CMG","HSY","LYV","IRM","WEC","VMC","SYY", | |
| "PYPL","PRU","HIG","PAYX","WAT","WDAY","MLM","UAL", | |
| "EQT","ROP","HBAN","ZTS","LVS","NTAP","KMB","HAL","MTB","ACGL", | |
| "CNC","EXR","IQV","DTE","AEE","EL","FSLR","VICI","RJF","CBOE", | |
| "GEHC","FISV","DOV","BIIB","CPRT","RMD","ATO","IR","EIX","VEEV", | |
| "DXCM","CNP","ARES","EXPE","NRG","CFG","OTIS","AVB","PPL", | |
| "TPL","JBHT","FE","VRSN","KHC","ES","FICO","XYL", | |
| "EQR","WRB","PPG","CTSH","HUBB","SMCI","WTW","BG","DOW","RF", | |
| "AWK","STZ","WSM","SYF","VRSK","KEY","HPQ","MTD","CHD","DG","PFG", | |
| "TROW","CPAY","DRI","ROL","PHM","NI","CMS","LEN","WST","DGX", | |
| "L","SBAC","RL","LH","AKAM","CHRW","OMC","FIS","ZS", | |
| "EXPD","DLTR","LYB","STE","EFX","INCY","INSM","LUV","TSN","ULTA","BRO", | |
| "PKG","SNA","DD","MRNA","LNT","IFF","VTRS","ESS","ALB", | |
| "CHTR","GPN","INVH","IP","GIS","WY","AMCR","BR","CF","CDW","ZBH", | |
| "HST","NVR","KIM","BEN","MAA","IEX","TXT","GEN","PTC","LDOS", | |
| "TSCO","GNRC","CPT","BBY","DECK","APTV","REG","BALL", | |
| "MAS","DOC","LULU","GPC","EG","TYL","HRL","APA", | |
| "AIZ","UDR","MKC","TRMB","PNW","GL","DVA","SWK","MGM","IVZ", | |
| "ALGN","HAS","AVY","PNR","HII","CLX","ALLE","GDDY","SWKS","ZBRA", | |
| "IT","RVTY","WYNN","FRT","PODD","ERIE","AES","DPZ","BAX","BXP", | |
| "TTD","FDS","JKHY","ARE","HSIC","UHS","CRL","NCLH","TECH","BLDR","AOS", | |
| "TAP","MOS","POOL","CPB","CAG" | |
| ] | |
| pump_stocks = [ | |
| "AACG", "AAME", "ABTS", "ACAA", "ACAAU", "ACAAW", "ACCL", "ACCS", "ACET", "ACFN", | |
| "ACNB", "ACNT", "ACON", "ACONW", "ACR", "ACTU", "ACU", "ACXP", "ADGM", "ADIL", | |
| "ADTX", "ADV", "ADVB", "ADXN", "AEHL", "AEI", "AEMD", "AENT", "AENTW", "AERT", | |
| "AERTW", "AEYE", "AFBI", "AFJK", "AFJKR", "AFJKU", "AFRI", "AFRIW", "AFYA", "AGCC", | |
| "AGIG", "AGL", "AGMH", "AGPU", "AGRZ", "AHMA", "AHT", "AIDX", "AIFA", "AIHS", | |
| "AIM", "AIMD", "AIMDW", "AIOS", "AIRE", "AIRI", "AIRT", "AIRTP", "AIXI", "AKA", | |
| "AKAN", "AKTX", "ALAR", "ALCO", "ALF", "ALFUU", "ALFUW", "ALGS", "ALIS", "ALISR", | |
| "ALISU", "ALOT", "ALP", "ALX", "ALZN", "AMBO", "AMCI", "AMS", "AMSS", "AMST", | |
| "ANGH", "ANGHW", "ANNA", "ANNAW", "ANTA", "ANY", "APAC", "APACR", "APACU", "APLM", | |
| "APM", "APRE", "APT", "APUR", "APURR", "APVO", "APWC", "AQB", "AQMS", "ARBB", | |
| "ARBK", "ARCI", "ARCIU", "ARCIW", "ARCL", "ARCLR", "ARCLU", "ARCLW", "ARKR", "ARL", | |
| "ARQQ", "ARQQW", "ARTL", "ARTNA", "ARTV", "ARTW", "ASBP", "ASBPW", "ASIC", "ASMB", | |
| "ASPC", "ASPCR", "ASPCU", "ASPS", "ASPSW", "ASPSZ", "ASR", "ASRT", "ASTC", "ASTI", | |
| "ATER", "ATGL", "ATHE", "ATHR", "ATLC", "ATLCP", "ATLO", "ATNI", "ATOS", "ATPC", | |
| "ATRA", "ATXG", "AUBN", "AUC", "AURE", "AUST", "AUUD", "AVBH", "AWX", "AXIL", | |
| "AXR", "AYTU", "AZI", "BAFN", "BANL", "BAOS", "BARK", "BAYA", "BAYAR", "BAYAU", | |
| "BBGI", "BBLG", "BBLGW", "BCAB", "BCDA", "BCG", "BCGWW", "BCTX", "BCTXL", "BCTXZ", | |
| "BDL", "BDMD", "BDMDW", "BDRX", "BDSX", "BEBE", "BESS", "BETR", "BETRW", "BFRI", | |
| "BFRIW", "BGDE", "BGI", "BGLC", "BGMS", "BGSF", "BH", "BHAV", "BHAVR", "BHAVU", | |
| "BHM", "BIAF", "BIAFW", "BIDWU", "BIRD", "BIVI", "BIVIW", "BIYA", "BJDX", "BKHA", | |
| "BKHAR", "BKHAU", "BKTI", "BLRX", "BMGL", "BMHL", "BMRA", "BNAI", "BNAIW", "BNBX", | |
| "BNKK", "BNR", "BNRG", "BNZI", "BNZIW", "BODI", "BOLT", "BON", "BOSC", "BOTJ", | |
| "BOXL", "BPACR", "BPRN", "BQ", "BREZ", "BREZR", "BRFH", "BRID", "BRLS", "BRLSW", | |
| "BRN", "BRUN", "BRUNW", "BSAA", "BSAAR", "BSAAU", "BSBK", "BSET", "BSVN", "BTBD", | |
| "BTCT", "BTOG", "BUDA", "BUUU", "BVFL", "BWFG", "BXC", "BYAH", "BYFC", "CAAS", | |
| "CABO", "CABR", "CACC", "CALC", "CANF", "CAPN", "CAPNR", "CAPNU", "CAPS", "CARL", | |
| "CBFV", "CBK", "CBNA", "CBRS", "CCAQ", "CCAQU", "CCAQW", "CCEC", "CCEL", "CCHH", | |
| "CCM", "CCTG", "CDIO", "CDIOW", "CDT", "CDTG", "CDTTW", "CELZ", "CENN", "CETX", | |
| "CFBK", "CFFI", "CHAI", "CHAR", "CHARR", "CHARU", "CHCI", "CHMG", "CHNR", "CHPG", | |
| "CHPGR", "CHPGU", "CHR", "CHRN", "CHSN", "CIIT", "CISS", "CIX", "CJMB", "CKX", | |
| "CLIK", "CLIR", "CLRB", "CLRO", "CLST", "CLWT", "CMCM", "CMCT", "CMDB", "CMMB", | |
| "CMND", "CMT", "CMTV", "CNET", "CNEY", "CNF", "CNSP", "COAG", "COCP", "CODA", | |
| "CODX", "COE", "COHN", "COKE", "COLA", "COLAR", "COLAU", "COOK", "COOT", "COOTW", | |
| "COSO", "CPBI", "CPHC", "CPIX", "CPSS", "CRAI", "CRE", "CREX", "CRMT", "CRT", | |
| "CRVO", "CRWS", "CSBR", "CSPI", "CTEV", "CTNT", "CTRN", "CTW", "CUE", "CULP", | |
| "CUPR", "CURX", "CVCO", "CVEO", "CVKD", "CVM", "CVR", "CVV", "CWD", "CXIIU", | |
| "CYAB", "CYCN", "CYCU", "CYCUW", "CZFS", "CZWI", "DAC", "DAIC", "DAICW", "DAIO", | |
| "DAVE", "DAVEW", "DCBO", "DCOY", "DDS", "DETX", "DEVS", "DFSC", "DFSCW", "DIT", | |
| "DJCO", "DKI", "DLHC", "DLPN", "DLTH", "DLXY", "DOGZ", "DOMH", "DRCT", "DRIO", | |
| "DRMA", "DRMAW", "DRUG", "DSS", "DSWL", "DTSQ", "DTSQR", "DTSQU", "DTSS", "DTST", | |
| "DUKR", "DUKRW", "DWSN", "DWTX", "DXR", "DXST", "EBMT", "EBON", "ECBK", "ECO", | |
| "ECOR", "EDBL", "EDBLW", "EDHL", "EDN", "EDRY", "EDSA", "EDTK", "EDUC", "EEIQ", | |
| "EFOI", "EFSI", "EFTY", "EGG", "EHGO", "EHLD", "EJH", "ELA", "ELAB", "ELLO", | |
| "ELMD", "ELMT", "ELOG", "ELOX", "ELPW", "ELTK", "ELWT", "EML", "EMPG", "ENGS", | |
| "ENVB", "EPSM", "EQS", "ERNA", "ERNAW", "EROC", "ESCA", "ESEA", "ESLAW", "ESP", | |
| "ESQ", "ETS", "ETSS", "EUDA", "EUDAW", "EURK", "EURKR", "EURKU", "EVAX", "EVI", | |
| "EXOD", "EXOZ", "EXYN", "EXYNW", "EZRA", "FABC", "FAMI", "FATN", "FBGL", "FBIZ", | |
| "FBLG", "FC", "FCAP", "FCCO", "FCNCA", "FCNCO", "FCNCP", "FCUV", "FDBC", "FDSB", | |
| "FEDU", "FEED", "FEIM", "FENG", "FET", "FGBI", "FGBIP", "FGI", "FGIWW", "FGL", | |
| "FGNX", "FGNXP", "FIEE", "FINW", "FKWL", "FLD", "FLDDW", "FLXS", "FLYE", "FMAC", | |
| "FMACR", "FMACU", "FMFC", "FNUC", "FNWB", "FNWD", "FOA", "FOFO", "FORTY", "FOXX", | |
| "FOXXW", "FRAF", "FRD", "FRGT", "FRMM", "FRSX", "FRTT", "FRVO", "FSBW", "FSEA", | |
| "FSHP", "FSHPR", "FSHPU", "FSI", "FSTR", "FTFT", "FTHAU", "FTLF", "FTRK", "FTW", | |
| "FUNC", "FUSB", "FVN", "FVNNR", "FVNNU", "FXNC", "GBFH", "GBLI", "GBR", "GCBC", | |
| "GCDT", "GCO", "GCTK", "GDEV", "GDEVW", "GDTC", "GELS", "GENK", "GHC", "GHG", | |
| "GIGM", "GIPR", "GIPRW", "GITS", "GLE", "GLED", "GLMD", "GLOO", "GLSI", "GLXG", | |
| "GMEX", "GMM", "GNLN", "GOVX", "GP", "GRAN", "GRI", "GROW", "GRVY", "GSBC", | |
| "GSIW", "GSRVU", "GSUN", "GTIM", "GURE", "GV", "GVH", "GWAV", "GXAI", "GYRO", | |
| "HAO", "HAWK", "HBB", "HBCP", "HBIO", "HCAI", "HCHL", "HCTI", "HCWB", "HDRN", | |
| "HDRNW", "HFBL", "HHS", "HIFS", "HIHO", "HIND", "HKIT", "HKPD", "HMH", "HMR", | |
| "HNNA", "HNVR", "HOUR", "HOV", "HPAI", "HPAIW", "HQI", "HSCS", "HSCSW", "HSPT", | |
| "HSPTR", "HSPTU", "HTCO", "HTCR", "HTLM", "HTOO", "HUBC", "HUBCZ", "HUDI", "HUHU", | |
| "HUIZ", "HURC", "HWBK", "HWH", "HWKN", "HXHX", "HYFM", "IBAC", "IBACR", "IBG", | |
| "IBTA", "ICCC", "ICCM", "ICON", "ICU", "ICUCW", "IDACU", "IDAI", "IESC", "IFBD", | |
| "IHT", "IIIV", "ILAG", "IMA", "IMCC", "IMNN", "IMRN", "IMTE", "INAB", "INBK", | |
| "INBS", "INBX", "INDO", "INDP", "INEO", "INHD", "INKT", "INLF", "INLX", "INM", | |
| "INSE", "INTG", "INTJ", "INTS", "IOR", "IOTR", "IPDN", "IPEX", "IPEXR", "IPEXU", | |
| "IPFX", "IPFXU", "IPFXW", "IPM", "IPST", "IPW", "IPWR", "IQST", "IRMD", "ISBA", | |
| "ISPC", "ITIC", "IVF", "IXHL", "IZM", "JAGX", "JAKK", "JBDI", "JBSS", "JCSE", | |
| "JCTC", "JDZG", "JEM", "JFB", "JFU", "JG", "JILL", "JL", "JLHL", "JOUT", "JRSH", | |
| "JTAI", "JVA", "JWEL", "JXG", "JYD", "JYNT", "JZ", "JZXN", "KARO", "KEQU", "KFFB", | |
| "KG", "KIDZ", "KIDZW", "KITT", "KITTW", "KLRA", "KLRS", "KMDA", "KMRK", "KORE", | |
| "KOSS", "KPLT", "KPRX", "KRKR", "KRRO", "KRT", "KRUS", "KUST", "KVAC", "KVACU", | |
| "KVACW", "KWY", "KXIN", "KYNB", "KZIA", "LABT", "LAKE", "LARK", "LBGJ", "LCCC", | |
| "LCCCR", "LCCCU", "LCFY", "LCFYW", "LEDS", "LEE", "LESL", "LFS", "LFVN", "LFWD", | |
| "LGCY", "LGHL", "LGL", "LGPS", "LHSW", "LINK", "LIQT", "LITB", "LIVE", "LNAI", | |
| "LNSR", "LNZA", "LOAN", "LOBO", "LOCL", "LONA", "LPA", "LPCN", "LRE", "LRHC", | |
| "LSBK", "LSE", "LSF", "LSTA", "LTRN", "LUCK", "LUCY", "LUCYW", "LUD", "LVLU", | |
| "LVO", "LWAY", "LXEH", "LZMH", "MAMK", "MAMO", "MARPS", "MASK", "MAYS", "MB", | |
| "MBAI", "MBBC", "MBIO", "MBRX", "MCFT", "MCRB", "MCTA", "MDBH", "MDRR", "MDV", | |
| "MDWD", "MED", "MEGL", "MFI", "MG", "MGIH", "MGRT", "MGYR", "MHH", "MI", "MIMI", | |
| "MIND", "MITQ", "MKDW", "MKDWW", "MKTW", "MKZR", "MLAB", "MLCI", "MLGO", "MLP", | |
| "MMTX", "MMTXU", "MMTXW", "MNDR", "MNPR", "MNSB", "MNSBP", "MNTS", "MNTSW", "MOBI", | |
| "MODD", "MOGU", "MOVE", "MPTI", "MRBK", "MRDN", "MRM", "MRNO", "MRNOW", "MSAI", | |
| "MSAIW", "MSGM", "MSGY", "MSN", "MSS", "MSW", "MTEK", "MTEKW", "MTEN", "MTEX", | |
| "MTNB", "MTR", "MTVA", "MVO", "MXC", "MYFW", "MYND", "MYSE", "MYSEW", "MYSZ", | |
| "MYX", "MYXXR", "MYXXU", "MYXXW", "NAAS", "NAII", "NAMM", "NAMMW", "NATH", "NBN", | |
| "NBRG", "NBRGR", "NBRGU", "NC", "NCEL", "NCEW", "NCI", "NCPL", "NCPLW", "NCRA", | |
| "NCSM", "NCT", "NCTY", "NDLS", "NDRA", "NEGG", "NEN", "NEPH", "NEU", "NEUP", | |
| "NEXR", "NEXRW", "NGNE", "NHTC", "NINE", "NIVF", "NIVFW", "NKSH", "NL", "NMP", | |
| "NMPAR", "NMPAU", "NMTC", "NODK", "NOEM", "NOEMR", "NOEMU", "NOEMW", "NOMA", | |
| "NPK", "NPT", "NRP", "NRT", "NRXS", "NSRX", "NSTS", "NSYS", "NTHI", "NTIC", | |
| "NTRB", "NTRBW", "NTRP", "NTWK", "NTZ", "NUTR", "NUTX", "NUWE", "NVEC", "NVNI", | |
| "NVNIW", "NVNO", "NVR", "NVVE", "NWFL", "NWPX", "NWTG", "NXB", "NXGL", "NXGLW", | |
| "NXPL", "NXTC", "NXTS", "NYC", "OCC", "OCG", "ODC", "OESX", "OFAL", "OFLX", | |
| "OGEN", "OLB", "OLOX", "ONCO", "ONEG", "ONFO", "ONFOW", "ONIT", "OPAD", "OPHC", | |
| "OPTH", "OPTX", "OPTXW", "OPXS", "OPY", "ORGN", "ORGNW", "ORIQ", "ORIQU", "ORIQW", | |
| "ORIS", "ORKT", "OST", "OVBC", "OVLY", "OXBR", "OXBRW", "OZ", "PALO", "PALOU", | |
| "PALOW", "PAMT", "PAPL", "PARK", "PASG", "PAVM", "PAVS", "PBFS", "PBHC", "PBM", | |
| "PBMWW", "PC", "PCLA", "PCSA", "PCSC", "PDC", "PDEX", "PEBK", "PECE", "PECER", | |
| "PECEU", "PECEW", "PED", "PETZ", "PFAI", "PFIS", "PFSA", "PFX", "PGAC", "PGACR", | |
| "PGACU", "PHGE", "PHOE", "PIII", "PIIIW", "PKBK", "PKOH", "PLBC", "PLBL", "PLCE", | |
| "PLPC", "PLRZ", "PLSM", "PLTS", "PLUR", "PLUT", "PLYX", "PMCB", "PMEC", "PMN", | |
| "PMTS", "PN", "PNRG", "POAS", "POCI", "PODC", "POLA", "PPBT", "PPCB", "PPIH", | |
| "PPSI", "PRFX", "PRHI", "PRHIZ", "PROV", "PRPO", "PRT", "PRTS", "PSIG", "PSIX", | |
| "PSTV", "PTACU", "PTHS", "PTLE", "PTN", "PTNM", "PULM", "PVLA", "PW", "PXED", | |
| "PXLW", "PXS", "QCLS", "QETA", "QETAR", "QETAU", "QH", "QNRX", "QNTM", "QRED", | |
| "QSEA", "QSEAR", "QSEAU", "QTI", "QTTB", "QUMS", "QUMSR", "QUMSU", "QXL", "RADX", | |
| "RAIN", "RAINW", "RAY", "RAYA", "RBCAA", "RBKB", "RBNE", "RCKY", "RCMT", "RDAC", | |
| "RDACR", "RDACU", "RDCM", "RDGT", "RDHL", "REA", "REBN", "RECT", "REED", "RENX", | |
| "RETO", "REVB", "REVBW", "RFAI", "RFAIR", "RFAIU", "RFAM", "RFAMR", "RFAMU", | |
| "RFIL", "RGCO", "RGNT", "RGS", "RHLD", "RIBB", "RIBBR", "RIBBU", "RICK", "RJET", | |
| "RKDA", "RLMD", "RLYB", "RM", "RMBI", "RMCF", "RMCO", "RMCOW", "RMSG", "RMSGW", | |
| "RNAZ", "ROLR", "RPGL", "RPT", "RRBI", "RTB", "RUBI", "RVSN", "RVSNW", "RYM", | |
| "RYOJ", "SAGT", "SAIH", "SAIHW", "SAM", "SAMG", "SBFG", "SBFM", "SBFMW", "SCKT", | |
| "SCLX", "SCLXW", "SCNI", "SCOR", "SCPQ", "SCPQU", "SCPQW", "SCYX", "SDEV", "SDHC", | |
| "SDOT", "SDST", "SDSTW", "SEAT", "SEATW", "SEB", "SEED", "SEG", "SEGG", "SENEA", | |
| "SENEB", "SER", "SFBC", "SFHG", "SFST", "SGA", "SGLY", "SGP", "SGRP", "SHAZ", | |
| "SHFS", "SHFSW", "SHIM", "SHPH", "SIF", "SILC", "SILO", "SIMA", "SIMAU", "SIMAW", | |
| "SINT", "SKBL", "SKIL", "SKK", "SKLZ", "SKYQ", "SLE", "SLGB", "SLGL", "SLNG", | |
| "SLXN", "SLXNW", "SMBC", "SMC", "SMID", "SMJF", "SMSI", "SMTI", "SMX", "SMXWW", | |
| "SNES", "SNOA", "SNSE", "SNTG", "SNWV", "SNYR", "SOBR", "SOGP", "SONM", "SORA", | |
| "SOTK", "SPCB", "SPEG", "SPEGU", "SPHL", "SPKL", "SPKLU", "SPKLW", "SPPL", "SPRB", | |
| "SPRC", "SPTX", "SQFT", "SQFTP", "SQFTW", "SRBK", "SRL", "SRZN", "SRZNW", "SSBI", | |
| "SSEA", "SSEAR", "SSEAU", "SSM", "SST", "SSTI", "STAK", "STEM", "STFS", "STG", | |
| "STHO", "STI", "STRR", "STRRP", "STRS", "STRT", "STRW", "SUGP", "SUNE", "SUNS", | |
| "SVRE", "SVRN", "SWAG", "SWAGW", "SWMR", "SWVL", "SXTC", "SXTP", "SYNX", "TACT", | |
| "TANH", "TAOP", "TAOX", "TAVI", "TAVIR", "TAVIU", "TAYD", "TBRG", "TC", "TCBS", | |
| "TCGL", "TCI", "TCRT", "TCX", "TDIC", "TDTH", "TGE", "TGL", "THCH", "TIL", | |
| "TISI", "TJGC", "TKLF", "TLF", "TLIH", "TMDE", "TNMG", "TOP", "TOPP", "TOPS", | |
| "TORO", "TOUR", "TOYO", "TPCS", "TPST", "TRNR", "TRNS", "TRSG", "TRT", "TRUG", | |
| "TRVG", "TSAT", "TSBK", "TULP", "TURB", "TVGN", "TVGNW", "TVRD", "TWAV", "TXMD", | |
| "TZOO", "UAC", "UBCP", "UBXG", "UCAR", "UCFI", "UCFIW", "UEIC", "UFG", "UFPT", | |
| "UG", "UGRO", "UI", "UK", "ULBI", "ULH", "UNB", "UNTY", "UONE", "UONEK", | |
| "UP", "UPC", "USEA", "USGO", "USNA", "UTMD", "UTSI", "UUU", "UYSC", "UYSCR", | |
| "UYSCU", "UZX", "VABK", "VALU", "VATE", "VBIO", "VCIG", "VEEE", "VEL", "VGAS", | |
| "VHC", "VHI", "VIDA", "VIRC", "VIVK", "VIVO", "VIVS", "VLGEA", "VMAR", "VNCE", | |
| "VNRX", "VRAX", "VRCA", "VRM", "VRTS", "VS", "VSA", "VSME", "VTAK", "VTVT", | |
| "WAFU", "WAI", "WATT", "WAVE", "WBUY", "WBX", "WCT", "WENC", "WEYS", "WFCF", | |
| "WFF", "WGRX", "WHG", "WHLR", "WHLRD", "WHLRL", "WHLRP", "WILC", "WINA", "WKEY", | |
| "WKHS", "WKSP", "WLDS", "WLDSW", "WLFC", "WNW", "WOK", "WRLC", "WSBK", "WSHP", | |
| "WSTN", "WSTNR", "WSTNU", "WTF", "WTG", "WTGUR", "WTGUU", "WTM", "WTO", "WVVI", | |
| "WVVIP", "WW", "WXM", "WYY", "XBIO", "XBP", "XBPEW", "XCUR", "XELB", "XHLD", | |
| "XLO", "XOMA", "XOMAO", "XOMAP", "XOS", "XRTX", "XTLB", "XWEL", "XXII", "YAAS", | |
| "YB", "YCY", "YDDL", "YDES", "YDESW", "YDKG", "YHGJ", "YHNA", "YHNAR", "YHNAU", | |
| "YI", "YIBO", "YJ", "YMAT", "YMT", "YQ", "YSXT", "YYAI", "ZBAO", "ZCMD", "ZEO", | |
| "ZEOWW", "ZEPP", "ZJK", "ZNB", "ZOOZ", "ZOOZW", "ZSQR", "ZSTK", "ZYBT", "SPCL", | |
| "UBXG", "EDHL", "DSY", "FRD", "SPCX", "AXTU", "AXTX", "AKAN", "ASTN", "CIFU", | |
| "CIFG", "SKIL", "ARMG", "POEL", "CRWU", "CWVX", "CRWG", "RNAC", "WPAC", "STXX", | |
| "AMKL", "ALBG", "KEEX", "STXL", "STXU", "NVTX", "VELL", "FLYT", "ASUP", "ASTY", | |
| "MNTS", "ASTX", "SATG", "ASTC", "LUNL", "CORD", "DXYZ", "QH", "AIBZ", "SDOT", | |
| "RDWU", "PLU", "HSPT", "YSS", "VELO", "FAC", "PCLA", "FLY", "RKLX", "ADBU", | |
| "OMER", "TVGN", "BVC" | |
| ] | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # Auto-scheduler โ fires every 15-min candle close during market hours | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| _auto_enabled = True | |
| _auto_lock = threading.Lock() | |
| _ET = ZoneInfo("America/New_York") | |
| def _now_et(): | |
| return datetime.now(_ET) | |
| def _is_market_hours(): | |
| now = _now_et() | |
| if now.weekday() >= 5: # Saturday / Sunday | |
| return False | |
| return dtime(9, 30) <= now.time() < dtime(16, 0) | |
| def _is_premarket(): | |
| now = _now_et() | |
| if now.weekday() >= 5: # Saturday / Sunday | |
| return False | |
| return dtime(4, 0) <= now.time() < dtime(9, 30) | |
| def _secs_to_next_close(): | |
| """Seconds until next :00/:15/:30/:45 boundary.""" | |
| now = _now_et() | |
| m = now.minute | |
| s = now.second + now.microsecond / 1_000_000 | |
| wait = ((m // 15) + 1) * 15 * 60 - m * 60 - s | |
| if wait <= 0: | |
| wait += 900 | |
| return wait | |
| def _fetch_all(stock_list): | |
| """Fetch all stocks in batches; return (results_dict, ok_count, fail_count).""" | |
| results = {} | |
| total_ok = 0 | |
| total_fail = 0 | |
| for i in range(0, len(stock_list), BATCH): | |
| batch = stock_list[i : i + BATCH] | |
| bd = fetch_batch(batch) | |
| results.update(bd) | |
| total_ok += len(bd) | |
| total_fail += len(batch) - len(bd) | |
| return results, total_ok, total_fail | |
| def _fetch_all_pm(stock_list): | |
| """Fetch all stocks (pre-market data) in batches; return (results_dict, ok, fail).""" | |
| results = {} | |
| total_ok = 0 | |
| total_fail = 0 | |
| for i in range(0, len(stock_list), BATCH): | |
| batch = stock_list[i : i + BATCH] | |
| bd = fetch_batch_pm(batch) | |
| results.update(bd) | |
| total_ok += len(bd) | |
| total_fail += len(batch) - len(bd) | |
| return results, total_ok, total_fail | |
| def _send_strat_alert(strat_fn, results, label, now_s, stats): | |
| """Run one strategy on pre-fetched data and send a message only if signals found.""" | |
| found = [] | |
| for sym, df in results.items(): | |
| r = strat_fn(sym, df, True) # auto scan โ dedup ON | |
| if r: | |
| found.append(r) | |
| if not found: | |
| return | |
| found.sort(key=lambda x: x["pct"], reverse=True) | |
| emoji = "๐" if "Closed" in label else "โก" | |
| hdr = ( | |
| f"{emoji} {label}\n" | |
| f"๐ {now_s} | {stats} | ๐ฏ {len(found)} ุฅุดุงุฑุฉ\n" | |
| f"{'โ' * 32}\n" | |
| ) | |
| send(hdr + "\n\n".join(r["msg"] for r in found)) | |
| def auto_scan_all(): | |
| if not _auto_lock.acquire(blocking=False): | |
| print("โ ๏ธ Auto scan skipped โ previous still running") | |
| return | |
| try: | |
| now_s = datetime.now(SAUDI_TZ).strftime("%H:%M KSA") | |
| print(f"๐ Auto scan @ {now_s}") | |
| # โโ Small Caps โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| sc_data, ok, fail = _fetch_all(pump_stocks) | |
| stats_sc = f"โ {ok} | โ {fail}" | |
| _send_strat_alert(strat_pump_closed, sc_data, | |
| "PUMP Closed 15M โ Small Caps", now_s, stats_sc) | |
| _send_strat_alert(strat_pump_live, sc_data, | |
| "PUMP LIVE 15M โ Small Caps", now_s, stats_sc) | |
| # โโ S&P 500 โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| sp_data, ok, fail = _fetch_all(sp500_stocks) | |
| stats_sp = f"โ {ok} | โ {fail}" | |
| _send_strat_alert(strat_pump_closed, sp_data, | |
| "PUMP Closed 15M โ S&P 500", now_s, stats_sp) | |
| _send_strat_alert(strat_pump_live, sp_data, | |
| "PUMP LIVE 15M โ S&P 500", now_s, stats_sp) | |
| finally: | |
| _auto_lock.release() | |
| _pm_auto_lock = threading.Lock() | |
| def auto_scan_pm(): | |
| """Auto pre-market scan โ runs every 15-min boundary during 4:00โ9:30 ET.""" | |
| if not _pm_auto_lock.acquire(blocking=False): | |
| print("โ ๏ธ PM auto scan skipped โ previous still running") | |
| return | |
| try: | |
| now_s = datetime.now(SAUDI_TZ).strftime("%H:%M KSA") | |
| print(f"๐ Pre-market auto scan @ {now_s}") | |
| sc_data, ok, fail = _fetch_all_pm(pump_stocks) | |
| stats_sc = f"โ {ok} | โ {fail}" | |
| _send_strat_alert(strat_pump_pm, sc_data, | |
| "Pre-market PUMP 15M โ Small Caps", now_s, stats_sc) | |
| sp_data, ok, fail = _fetch_all_pm(sp500_stocks) | |
| stats_sp = f"โ {ok} | โ {fail}" | |
| _send_strat_alert(strat_pump_pm, sp_data, | |
| "Pre-market PUMP 15M โ S&P 500", now_s, stats_sp) | |
| finally: | |
| _pm_auto_lock.release() | |
| def candle_scheduler(): | |
| print("โฐ Candle scheduler started (fires on :00/:15/:30/:45 ET, market hours only)") | |
| while True: | |
| wait = _secs_to_next_close() + 10 # +10s buffer for data propagation | |
| time.sleep(wait) | |
| if _auto_enabled and _is_market_hours(): | |
| threading.Thread(target=auto_scan_all, daemon=True).start() | |
| elif _auto_enabled and _is_premarket(): | |
| threading.Thread(target=auto_scan_pm, daemon=True).start() | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # Telegram polling | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def poll(): | |
| global _auto_enabled | |
| last_id = None | |
| while True: | |
| try: | |
| params = {"timeout": 5} | |
| if last_id is not None: | |
| params["offset"] = last_id + 1 | |
| res = _tg.get( | |
| f"https://api.telegram.org/bot{BOT_TOKEN}/getUpdates", | |
| params=params, timeout=15 | |
| ).json() | |
| for u in res.get("result", []): | |
| last_id = u["update_id"] | |
| if "message" in u: | |
| text = u["message"].get("text", "").strip() | |
| cid = str(u["message"]["chat"]["id"]) | |
| if text in ("/start", "/help"): | |
| mh = "โ ู ูุชูุญ" if _is_market_hours() else ("๐ Pre-market" if _is_premarket() else "โ ู ุบูู") | |
| nx_min = int(_secs_to_next_close() / 60) | |
| nx_sec = int(_secs_to_next_close() % 60) | |
| send( | |
| "๐ ุจูุช ู ุฑุงูุจุฉ ุงูุฃุณูู โ PUMP 15M\n\n" | |
| "๐ค ุงููุญุต ุงูุชููุงุฆู ูุนู ู ูู ุฅุบูุงู ุดู ุนุฉ 15 ุฏูููุฉ\n" | |
| f" ุงูุณูู: {mh} | ุงูุดู ุนุฉ ุงููุงุฏู ุฉ: {nx_min}ุฏ {nx_sec}ุซ\n\n" | |
| "ุงูุฃูุงู ุฑ:\n" | |
| "/today โ ุฃุณูู pumped ุงูููู โฅ15% (market hours)\n" | |
| "/premarket โ ุฃุณูู pumped ุงูููู โฅ10% (pre-market)\n" | |
| "/update โ ูุญุต ูุฏูู ุจุงุฎุชูุงุฑ ุงูุงุณุชุฑุงุชูุฌูุฉ\n" | |
| "/auto โ ุชูุนูู/ุฅููุงู ุงููุญุต ุงูุชููุงุฆู\n" | |
| "/status โ ุญุงูุฉ ุงูุจูุช ูุงูุณูู ุงูุขู\n" | |
| "/test SYMBOL โ ูุญุต ุณูู ู ุจุงุดุฑ\n\n" | |
| "๐ Market Hours: 15ุฏ (9:30โ16:00 ET) โ ุดุฑุท โฅ15%\n" | |
| "๐ Pre-market: 15ุฏ (4:00โ9:30 ET) โ ุดุฑุท โฅ10%\n" | |
| "๐ง ูุนุฑุถ ุงูุณูููุฉ ููู ุฅุดุงุฑุฉ", | |
| cid | |
| ) | |
| elif text == "/status": | |
| if _is_market_hours(): | |
| mh = "โ ู ูุชูุญ ุงูุขู (Market Hours)" | |
| elif _is_premarket(): | |
| mh = "๐ Pre-market ุงูุขู (4:00โ9:30 ET)" | |
| else: | |
| mh = "โ ู ุบูู ุงูุขู" | |
| auto = "๐ข ุชููุงุฆู: ู ูุนูู" if _auto_enabled else "๐ด ุชููุงุฆู: ู ูููู" | |
| nx = _secs_to_next_close() | |
| now_s = datetime.now(SAUDI_TZ).strftime("%H:%M:%S KSA") | |
| now_et_s = _now_et().strftime("%H:%M:%S ET") | |
| send( | |
| f"๐ ุญุงูุฉ ุงูุจูุช โ {now_s}\n" | |
| f" ุงูููุช ET: {now_et_s}\n" | |
| f" ุงูุณูู: {mh}\n" | |
| f" {auto}\n" | |
| f" ุงูุดู ุนุฉ ุงููุงุฏู ุฉ ุชุบูู ูู: {int(nx//60)}ุฏ {int(nx%60)}ุซ\n\n" | |
| f"๐ ููุงุฆู ุงูุฃุณูู :\n" | |
| f" Small Caps: {len(pump_stocks):,} ุณูู \n" | |
| f" S&P 500: {len(sp500_stocks):,} ุณูู \n\n" | |
| f"โฐ ุงูุฌุฏูู ุงูุชููุงุฆู:\n" | |
| f" ๐ Pre-market: 4:00โ9:30 ET (โฅ10%)\n" | |
| f" ๐ Market Hours: 9:30โ16:00 ET (โฅ15%)", | |
| cid | |
| ) | |
| elif text == "/auto": | |
| _auto_enabled = not _auto_enabled | |
| st = "๐ข ู ูุนูู โ ุณูุฑุณู ุชูุจููุงุช ุชููุงุฆูุงู ุนูุฏ ูู ุฅุบูุงู ุดู ุนุฉ 15ุฏ" \ | |
| if _auto_enabled else \ | |
| "๐ด ู ูููู โ ุงุณุชุฎุฏู /update ูููุญุต ุงููุฏูู" | |
| send(f"ุงููุญุต ุงูุชููุงุฆู: {st}", cid) | |
| elif text == "/today": | |
| today_s = _now_et().strftime("%a %b %d") | |
| send( | |
| f"๐ {today_s} โ ุฌุงุฑู ูุญุต ูู ุงูุฃุณูู \n" | |
| f" Small Caps: {len(pump_stocks):,} | S&P 500: {len(sp500_stocks):,}\n" | |
| f" ูุจุญุซ ุนู ุฃู ุดู ุนุฉ โฅ15% ู ูุฐ ูุชุญ ุงูุณูู...", | |
| cid | |
| ) | |
| def _today_scan(cid=cid, today_s=today_s): | |
| sc = run_scan(strat_pump_closed, pump_stocks, | |
| f"๐ Pumps ุงูููู ({today_s}) โ Small Caps", cid) or 0 | |
| sp = run_scan(strat_pump_closed, sp500_stocks, | |
| f"๐ Pumps ุงูููู ({today_s}) โ S&P 500", cid) or 0 | |
| if sc + sp == 0: | |
| send("๐ญ ูุง ุชูุฌุฏ ุฃุณูู pumped โฅ15% ุงูููู ุญุชู ุงูุขู.", cid) | |
| threading.Thread(target=_today_scan, daemon=True).start() | |
| elif text in ("/premarket", "/pm"): | |
| today_s = _now_et().strftime("%a %b %d") | |
| send( | |
| f"๐ Pre-market {today_s} โ ุฌุงุฑู ูุญุต ูู ุงูุฃุณูู \n" | |
| f" Small Caps: {len(pump_stocks):,} | S&P 500: {len(sp500_stocks):,}\n" | |
| f" ูุจุญุซ ุนู ุฃู ุดู ุนุฉ โฅ10% ูู ุงูู Pre-market (4:00โ9:30 ET)...", | |
| cid | |
| ) | |
| def _pm_scan(cid=cid, today_s=today_s): | |
| _scan_lock_pm = threading.Lock() | |
| found_total = 0 | |
| # Small Caps PM | |
| total_sc = len(pump_stocks) | |
| batches_sc = [pump_stocks[i:i+BATCH] for i in range(0, total_sc, BATCH)] | |
| found_sc = [] | |
| ok_sc = fail_sc = 0 | |
| msg_id = send_id( | |
| f"๐ Pre-market ูุญุต โ Small Caps\n{pbar(0, total_sc)}\n0/{total_sc}", cid | |
| ) | |
| import time as _t; start = _t.time() | |
| for bi, batch in enumerate(batches_sc): | |
| bd = fetch_batch_pm(batch) | |
| ok_sc += len(bd); fail_sc += len(batch) - len(bd) | |
| for sym, df in bd.items(): | |
| r = strat_pump_pm(sym, df, False) | |
| if r: found_sc.append(r) | |
| done = min((bi+1)*BATCH, total_sc) | |
| if msg_id: | |
| edit(cid, msg_id, | |
| f"๐ Pre-market โ Small Caps\n{pbar(done,total_sc)}\n" | |
| f"{done}/{total_sc} โ {ok_sc} โ{fail_sc} ๐ฏ{len(found_sc)}") | |
| elapsed = int(_t.time()-start) | |
| if found_sc: | |
| found_sc.sort(key=lambda x: x["pct"], reverse=True) | |
| hdr = (f"๐ Pre-market Pumps ({today_s}) โ Small Caps\n" | |
| f"โ {ok_sc} | โ{fail_sc} | โฑ{elapsed}ุซ\n{'โ'*28}\n") | |
| send(hdr + "\n\n".join(r["msg"] for r in found_sc), cid) | |
| else: | |
| send(f"๐ ูุง ุฅุดุงุฑุงุช Pre-market โฅ10% โ Small Caps ({today_s})", cid) | |
| found_total += len(found_sc) | |
| # S&P 500 PM | |
| total_sp = len(sp500_stocks) | |
| batches_sp = [sp500_stocks[i:i+BATCH] for i in range(0, total_sp, BATCH)] | |
| found_sp = [] | |
| ok_sp = fail_sp = 0 | |
| msg_id2 = send_id( | |
| f"๐ Pre-market ูุญุต โ S&P 500\n{pbar(0, total_sp)}\n0/{total_sp}", cid | |
| ) | |
| start = _t.time() | |
| for bi, batch in enumerate(batches_sp): | |
| bd = fetch_batch_pm(batch) | |
| ok_sp += len(bd); fail_sp += len(batch) - len(bd) | |
| for sym, df in bd.items(): | |
| r = strat_pump_pm(sym, df, False) | |
| if r: found_sp.append(r) | |
| done = min((bi+1)*BATCH, total_sp) | |
| if msg_id2: | |
| edit(cid, msg_id2, | |
| f"๐ Pre-market โ S&P 500\n{pbar(done,total_sp)}\n" | |
| f"{done}/{total_sp} โ {ok_sp} โ{fail_sp} ๐ฏ{len(found_sp)}") | |
| elapsed = int(_t.time()-start) | |
| if found_sp: | |
| found_sp.sort(key=lambda x: x["pct"], reverse=True) | |
| hdr = (f"๐ Pre-market Pumps ({today_s}) โ S&P 500\n" | |
| f"โ {ok_sp} | โ{fail_sp} | โฑ{elapsed}ุซ\n{'โ'*28}\n") | |
| send(hdr + "\n\n".join(r["msg"] for r in found_sp), cid) | |
| else: | |
| send(f"๐ ูุง ุฅุดุงุฑุงุช Pre-market โฅ10% โ S&P 500 ({today_s})", cid) | |
| found_total += len(found_sp) | |
| if found_total == 0: | |
| send("๐ญ ูุง ุชูุฌุฏ ุฃุณูู pumped โฅ10% ูู ุงูู Pre-market ุงูููู .", cid) | |
| threading.Thread(target=_pm_scan, daemon=True).start() | |
| elif text == "/update": | |
| keyboard( | |
| "๐ ุงุฎุชุฑ ุงูุงุณุชุฑุงุชูุฌูุฉ:", | |
| [ | |
| [{"text": "๐ PUMP Closed โ Small Caps", "callback_data": "pump_sc"}], | |
| [{"text": "โก PUMP LIVE โ Small Caps", "callback_data": "live_sc"}], | |
| [{"text": "๐ PUMP Closed โ S&P 500", "callback_data": "pump_sp"}], | |
| [{"text": "โก PUMP LIVE โ S&P 500", "callback_data": "live_sp"}], | |
| [{"text": "๐ Pre-market PUMP โ Small Caps", "callback_data": "pm_sc"}], | |
| [{"text": "๐ Pre-market PUMP โ S&P 500", "callback_data": "pm_sp"}], | |
| ], | |
| cid | |
| ) | |
| elif text.startswith("/test"): | |
| parts = text.split() | |
| sym = parts[1] if len(parts) >= 2 else None | |
| if sym: | |
| threading.Thread(target=cmd_test, args=(sym, cid), daemon=True).start() | |
| else: | |
| send("ุฃุฑุณู ุฑู ุฒ ุงูุณูู ู ุน ุงูุฃู ุฑุ ู ุซุงู:\n/test NVDA", cid) | |
| elif "callback_query" in u: | |
| cq = u["callback_query"] | |
| answer_cb(cq["id"]) | |
| data = cq.get("data") | |
| cid = str(cq["message"]["chat"]["id"]) | |
| if data in STRATEGIES: | |
| strat_fn_name, stocks_name, label, is_pm = STRATEGIES[data] | |
| stocks = sp500_stocks if stocks_name == "sp500_stocks" else pump_stocks | |
| scan_fn = run_scan_pm if is_pm else run_scan | |
| threading.Thread( | |
| target=scan_fn, | |
| args=(strat_fn_name, stocks, label, cid), | |
| daemon=True | |
| ).start() | |
| except Exception as e: | |
| print(f"Poll error: {e}") | |
| time.sleep(3) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # Flask โ Health-check server (must respond fast, no external deps) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| app = Flask(__name__) | |
| app.logger.disabled = True # silence Flask access log noise | |
| def ping(): | |
| """Cron-job health check โ returns HTTP 200 in < 1 ms, no external deps.""" | |
| return "OK", 200 | |
| def home(): | |
| mh = "open" if _is_market_hours() else "closed" | |
| aut = "on" if _auto_enabled else "off" | |
| return f"Bot is running | market={mh} | auto={aut}", 200 | |
| def http_status(): | |
| now_s = datetime.now(SAUDI_TZ).strftime("%H:%M:%S KSA") | |
| now_et_s = _now_et().strftime("%H:%M:%S ET") | |
| mh = _is_market_hours() | |
| nx = int(_secs_to_next_close()) | |
| return { | |
| "status": "running", | |
| "time_ksa": now_s, | |
| "time_et": now_et_s, | |
| "market_open": mh, | |
| "auto_scan": _auto_enabled, | |
| "next_candle_secs": nx, | |
| "pump_stocks": len(pump_stocks), | |
| "sp500_stocks": len(sp500_stocks), | |
| }, 200 | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # Startup | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def _set_commands(): | |
| """Register bot commands so they appear when the user types / in Telegram.""" | |
| commands = [ | |
| {"command": "start", "description": "ุนุฑุถ ุงูู ุณุงุนุฏุฉ ููุงุฆู ุฉ ุงูุฃูุงู ุฑ"}, | |
| {"command": "help", "description": "ุนุฑุถ ุงูู ุณุงุนุฏุฉ ููุงุฆู ุฉ ุงูุฃูุงู ุฑ"}, | |
| {"command": "status", "description": "ุญุงูุฉ ุงูุจูุช ูุงูุณูู ุงูุขู"}, | |
| {"command": "today", "description": "ุฃุณูู pumped ุงูููู โฅ15% (market hours)"}, | |
| {"command": "premarket", "description": "ุฃุณูู pumped ุงูููู โฅ10% ูู ุงูู Pre-market"}, | |
| {"command": "pm", "description": "ุงุฎุชุตุงุฑ /premarket"}, | |
| {"command": "update", "description": "ูุญุต ูุฏูู โ ุงุฎุชุฑ ุงูุงุณุชุฑุงุชูุฌูุฉ"}, | |
| {"command": "auto", "description": "ุชูุนูู ุฃู ุฅููุงู ุงููุญุต ุงูุชููุงุฆู"}, | |
| {"command": "test", "description": "ูุญุต ุณูู ู ุจุงุดุฑ โ ู ุซุงู: /test NVDA"}, | |
| ] | |
| result = _post("setMyCommands", json={"commands": commands}) | |
| if result.get("result"): | |
| print("โ Bot commands registered successfully") | |
| else: | |
| print(f"โ ๏ธ setMyCommands failed: {result}") | |
| def _startup(): | |
| """Send startup Telegram message; errors here must not kill the process.""" | |
| try: | |
| _set_commands() | |
| mh_stat = "โ ุงูุณูู ู ูุชูุญ" if _is_market_hours() else "โ ุงูุณูู ู ุบูู" | |
| nx_min = int(_secs_to_next_close() / 60) | |
| send( | |
| f"๐ค ุงูุจูุช ุฌุงูุฒ! โ {datetime.now(SAUDI_TZ).strftime('%H:%M KSA')}\n" | |
| f"{mh_stat} | ุงูุดู ุนุฉ ุงููุงุฏู ุฉ: {nx_min}ุฏ\n" | |
| f"โฐ ุงููุญุต ุงูุชููุงุฆู ู ูุนูู (ูู 15ุฏ market hours)\n" | |
| f"/update ูููุญุต ุงููุฏูู | /status ููุญุงูุฉ | /ping ููู health-check" | |
| ) | |
| except Exception as e: | |
| print(f"Startup message error (non-fatal): {e}") | |
| print("๐ค PUMP Bot starting (yfinance engine)โฆ") | |
| threading.Thread(target=_startup, daemon=True).start() | |
| threading.Thread(target=poll, daemon=True).start() | |
| threading.Thread(target=candle_scheduler, daemon=True).start() | |
| if __name__ == "__main__": | |
| port = int(os.environ.get("PORT", 5000)) | |
| print(f"๐ Flask listening on 0.0.0.0:{port} โ /ping for health check") | |
| app.run(host="0.0.0.0", port=port, threaded=True) |