bot / bot.py
Monar1's picture
Create bot.py
93d7e29 verified
Raw History Blame Contribute Delete
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
@app.route("/ping")
def ping():
"""Cron-job health check โ€” returns HTTP 200 in < 1 ms, no external deps."""
return "OK", 200
@app.route("/")
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
@app.route("/status")
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)