Telegram: subscriber channel feed, owner lock, full risk disclaimer
- TELEGRAM_CHANNEL_ID mirrors auto-scan signal + breach alerts to a subscriber channel (Stars-subscription monetization; Telegram manages membership). Feed keeps running even when the owner's DM auto-scan is toggled off. - _owner_only decorator locks all commands/buttons to TELEGRAM_CHAT_ID; others get a refusal pointing at the channel. - Every signal-bearing message (detailed signal, scan summary, all-TF summary, breach alerts) now ends with a not-financial-advice / educational-use disclaimer via a single _DISCLAIMER constant.
This commit is contained in:
@@ -86,7 +86,8 @@ python backtest/gpu_backtest.py --pair EURUSD --primary-tf 5min --confirm-tfs 15
|
|||||||
|----------|---------|---------|
|
|----------|---------|---------|
|
||||||
| `TWELVE_DATA_API_KEY` | Market data API key | Hardcoded fallback in config |
|
| `TWELVE_DATA_API_KEY` | Market data API key | Hardcoded fallback in config |
|
||||||
| `TELEGRAM_TOKEN` | Telegram bot token | Hardcoded fallback |
|
| `TELEGRAM_TOKEN` | Telegram bot token | Hardcoded fallback |
|
||||||
| `TELEGRAM_CHAT_ID` | Auto-scan chat ID (enables boot auto-scan) | Hardcoded fallback |
|
| `TELEGRAM_CHAT_ID` | Owner chat ID (enables boot auto-scan; only this user can drive the bot) | Hardcoded fallback |
|
||||||
|
| `TELEGRAM_CHANNEL_ID` | Subscriber channel ID (`-100…`); signal + breach alerts mirror there; `""` disables | Hardcoded fallback |
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
@@ -106,6 +107,8 @@ python backtest/gpu_backtest.py --pair EURUSD --primary-tf 5min --confirm-tfs 15
|
|||||||
- **Manual scan:** Buttons for M15/H1/H4 individual scans + "Scan All TFs" multi-timeframe summary
|
- **Manual scan:** Buttons for M15/H1/H4 individual scans + "Scan All TFs" multi-timeframe summary
|
||||||
- **WebSocket:** Streams live ticks for XAU/USD; triggers immediate M15 scan on key level breach (swing S/R, BB, KC, EMA200) with 5-min cooldown
|
- **WebSocket:** Streams live ticks for XAU/USD; triggers immediate M15 scan on key level breach (swing S/R, BB, KC, EMA200) with 5-min cooldown
|
||||||
- **Commands:** `/start`, `/scan`, `/autoscan`, `/status`, `/cache`, `/clearcache`, `/help`
|
- **Commands:** `/start`, `/scan`, `/autoscan`, `/status`, `/cache`, `/clearcache`, `/help`
|
||||||
|
- **Owner lock:** all commands/buttons respond only to `TELEGRAM_CHAT_ID`; others get a refusal message
|
||||||
|
- **Subscriber channel:** with `TELEGRAM_CHANNEL_ID` set, auto-scan + breach alerts also post to the channel (bot must be channel admin). Monetized via Telegram Stars subscription invite links — Telegram handles membership; the bot has no payment code. Channel feed keeps running even if the owner's DM auto-scan is off
|
||||||
|
|
||||||
## Backtesting
|
## Backtesting
|
||||||
|
|
||||||
|
|||||||
+139
-36
@@ -18,12 +18,26 @@ WebSocket: Streams live ticks from TwelveData for XAU/USD.
|
|||||||
immediate M15 scan and sends a Level Break Alert.
|
immediate M15 scan and sends a Level Break Alert.
|
||||||
5-minute cooldown prevents spam.
|
5-minute cooldown prevents spam.
|
||||||
|
|
||||||
|
Owner lock: All commands and buttons only respond to TELEGRAM_CHAT_ID
|
||||||
|
(the owner). Everyone else gets a polite refusal — the bot
|
||||||
|
is a private control panel, not the product.
|
||||||
|
Channel: If TELEGRAM_CHANNEL_ID is set (-100xxxxxxxxxx), auto-scan
|
||||||
|
signal alerts and level-breach alerts are also posted to
|
||||||
|
that channel. Pair it with a Telegram Stars subscription
|
||||||
|
invite link to sell access — Telegram manages join/renewal/
|
||||||
|
kick; the bot just posts there (needs channel-admin +
|
||||||
|
post-messages). Manual scans, status and cache replies stay
|
||||||
|
owner-DM only. The channel feed runs even when the owner
|
||||||
|
toggles their own DM auto-scan off.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python telegram_bot.py
|
python telegram_bot.py
|
||||||
TELEGRAM_TOKEN=<token> TELEGRAM_CHAT_ID=<id> python telegram_bot.py
|
TELEGRAM_TOKEN=<token> TELEGRAM_CHAT_ID=<id> \
|
||||||
|
TELEGRAM_CHANNEL_ID=<-100…> python telegram_bot.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import functools
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import logging.handlers
|
import logging.handlers
|
||||||
@@ -79,6 +93,18 @@ logger = logging.getLogger("telegram_bot")
|
|||||||
TOKEN = os.environ.get("TELEGRAM_TOKEN", "8228251480:AAFW2OAyQ2idMicql2t_CIXk1r9-qfRTfNE")
|
TOKEN = os.environ.get("TELEGRAM_TOKEN", "8228251480:AAFW2OAyQ2idMicql2t_CIXk1r9-qfRTfNE")
|
||||||
CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "6025899022")
|
CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "6025899022")
|
||||||
|
|
||||||
|
# Owner lock: only this user/chat may drive the bot (commands + buttons).
|
||||||
|
# The bot is the product's signal source — without the lock, anyone who
|
||||||
|
# finds it could /autoscan and receive paid signals for free in DM.
|
||||||
|
OWNER_ID: Optional[int] = int(CHAT_ID) if CHAT_ID else None
|
||||||
|
|
||||||
|
# Subscriber channel (paid, Stars-subscription invite link managed by
|
||||||
|
# Telegram). When set, auto-scan signal alerts and level-breach alerts
|
||||||
|
# are also posted there. Manual scans, status, cache and boot messages
|
||||||
|
# stay owner-DM only. Set TELEGRAM_CHANNEL_ID="" to disable the feed.
|
||||||
|
_CHANNEL_ID_RAW = os.environ.get("TELEGRAM_CHANNEL_ID", "-1003942265371")
|
||||||
|
CHANNEL_ID: Optional[int] = int(_CHANNEL_ID_RAW) if _CHANNEL_ID_RAW else None
|
||||||
|
|
||||||
# ── Pairs & buttons ───────────────────────────────────────────────────
|
# ── Pairs & buttons ───────────────────────────────────────────────────
|
||||||
# The scanned pair list is config-driven: PlatformConfig.scan_pairs is
|
# The scanned pair list is config-driven: PlatformConfig.scan_pairs is
|
||||||
# the single switch for single- vs multi-pair operation. Buttons, WS
|
# the single switch for single- vs multi-pair operation. Buttons, WS
|
||||||
@@ -225,6 +251,13 @@ def _live_price_line(symbol: str) -> Optional[str]:
|
|||||||
return f"📡 Live: *{_fmt_price(price)}* ({age:.0f}s tick)"
|
return f"📡 Live: *{_fmt_price(price)}* ({age:.0f}s tick)"
|
||||||
|
|
||||||
|
|
||||||
|
_DISCLAIMER = (
|
||||||
|
"⚠️ _Not financial advice. For educational and research purposes only. "
|
||||||
|
"Trading forex/CFDs carries a high risk of loss — do your own analysis "
|
||||||
|
"and never risk money you cannot afford to lose._"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def format_signal_message(report: SignalReport) -> str:
|
def format_signal_message(report: SignalReport) -> str:
|
||||||
sig = report.final_signal
|
sig = report.final_signal
|
||||||
emoji = _SIGNAL_EMOJI.get(sig, "⬜")
|
emoji = _SIGNAL_EMOJI.get(sig, "⬜")
|
||||||
@@ -310,8 +343,7 @@ def format_signal_message(report: SignalReport) -> str:
|
|||||||
f"behind best price — TP1 is a guide, let it run"
|
f"behind best price — TP1 is a guide, let it run"
|
||||||
)
|
)
|
||||||
|
|
||||||
lines += ["", "━━━━━━━━━━━━━━━━━━━━",
|
lines += ["", "━━━━━━━━━━━━━━━━━━━━", _DISCLAIMER]
|
||||||
"_Educational / research use only._"]
|
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
@@ -342,6 +374,8 @@ def _format_scan_summary(reports: List[SignalReport], tf: Timeframe) -> str:
|
|||||||
else:
|
else:
|
||||||
lines.append("No actionable signals this cycle.")
|
lines.append("No actionable signals this cycle.")
|
||||||
|
|
||||||
|
lines += ["", _DISCLAIMER]
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
@@ -477,6 +511,20 @@ _autoscan_chats: Set[int] = set()
|
|||||||
_autoscan_tasks: list[asyncio.Task] = []
|
_autoscan_tasks: list[asyncio.Task] = []
|
||||||
|
|
||||||
|
|
||||||
|
def _feed_active() -> bool:
|
||||||
|
"""Scan loops keep running while anyone is listening: owner DM
|
||||||
|
auto-scan chats or the subscriber channel."""
|
||||||
|
return bool(_autoscan_chats) or CHANNEL_ID is not None
|
||||||
|
|
||||||
|
|
||||||
|
def _signal_targets() -> List[int]:
|
||||||
|
"""Where signal/breach alerts go: owner auto-scan chats + channel."""
|
||||||
|
targets: List[int] = list(_autoscan_chats)
|
||||||
|
if CHANNEL_ID is not None:
|
||||||
|
targets.append(CHANNEL_ID)
|
||||||
|
return targets
|
||||||
|
|
||||||
|
|
||||||
def _seconds_until_boundary(interval_minutes: int, buffer: int = 10) -> float:
|
def _seconds_until_boundary(interval_minutes: int, buffer: int = 10) -> float:
|
||||||
"""Seconds until the next candle close + buffer."""
|
"""Seconds until the next candle close + buffer."""
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
@@ -515,7 +563,7 @@ async def _scan_timeframe(
|
|||||||
if _passes_quality(r.signal_quality)
|
if _passes_quality(r.signal_quality)
|
||||||
and _should_alert(f"{r.symbol}_{tf.value}", r)
|
and _should_alert(f"{r.symbol}_{tf.value}", r)
|
||||||
]
|
]
|
||||||
for chat_id in list(_autoscan_chats):
|
for chat_id in _signal_targets():
|
||||||
for report in actionable:
|
for report in actionable:
|
||||||
detail = format_signal_message(report)
|
detail = format_signal_message(report)
|
||||||
try:
|
try:
|
||||||
@@ -610,7 +658,7 @@ async def _handle_breach(bot, ws_symbol: str, pair_symbol: str, price: float, le
|
|||||||
f"*{_escape_md(label)}* @ `{_fmt_price(price)}`\n\n"
|
f"*{_escape_md(label)}* @ `{_fmt_price(price)}`\n\n"
|
||||||
)
|
)
|
||||||
detail = header + format_signal_message(report)
|
detail = header + format_signal_message(report)
|
||||||
for chat_id in list(_autoscan_chats):
|
for chat_id in _signal_targets():
|
||||||
try:
|
try:
|
||||||
await bot.send_message(chat_id, detail, parse_mode=ParseMode.MARKDOWN)
|
await bot.send_message(chat_id, detail, parse_mode=ParseMode.MARKDOWN)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -627,7 +675,7 @@ async def _ws_heartbeat(ws) -> None:
|
|||||||
|
|
||||||
async def _ws_monitor(bot) -> None:
|
async def _ws_monitor(bot) -> None:
|
||||||
backoff = 1
|
backoff = 1
|
||||||
while _autoscan_chats:
|
while _feed_active():
|
||||||
hb_task: Optional[asyncio.Task] = None
|
hb_task: Optional[asyncio.Task] = None
|
||||||
try:
|
try:
|
||||||
async with websockets.connect(_WS_URL) as ws:
|
async with websockets.connect(_WS_URL) as ws:
|
||||||
@@ -641,7 +689,7 @@ async def _ws_monitor(bot) -> None:
|
|||||||
hb_task = asyncio.create_task(_ws_heartbeat(ws))
|
hb_task = asyncio.create_task(_ws_heartbeat(ws))
|
||||||
|
|
||||||
async for raw in ws:
|
async for raw in ws:
|
||||||
if not _autoscan_chats:
|
if not _feed_active():
|
||||||
break
|
break
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -699,11 +747,11 @@ async def _ws_monitor(bot) -> None:
|
|||||||
async def _candle_loop(bot, tf: Timeframe, interval_minutes: int) -> None:
|
async def _candle_loop(bot, tf: Timeframe, interval_minutes: int) -> None:
|
||||||
label = tf.value
|
label = tf.value
|
||||||
try:
|
try:
|
||||||
while _autoscan_chats:
|
while _feed_active():
|
||||||
wait = _seconds_until_boundary(interval_minutes, buffer=10)
|
wait = _seconds_until_boundary(interval_minutes, buffer=10)
|
||||||
logger.info(f"{label} auto-scan: next in {wait:.0f}s")
|
logger.info(f"{label} auto-scan: next in {wait:.0f}s")
|
||||||
await asyncio.sleep(wait)
|
await asyncio.sleep(wait)
|
||||||
if not _autoscan_chats:
|
if not _feed_active():
|
||||||
break
|
break
|
||||||
logger.info(f"{label} auto-scan: firing")
|
logger.info(f"{label} auto-scan: firing")
|
||||||
await _scan_timeframe(
|
await _scan_timeframe(
|
||||||
@@ -758,6 +806,35 @@ def _stop_scheduler() -> None:
|
|||||||
|
|
||||||
# ── Handlers ──────────────────────────────────────────────────────────
|
# ── Handlers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _owner_only(handler):
|
||||||
|
"""Reject commands/buttons from anyone but the owner.
|
||||||
|
|
||||||
|
Signals are sold via the subscriber channel; the bot itself is the
|
||||||
|
owner's control panel. Without this, any user who finds the bot
|
||||||
|
could /autoscan and receive the paid feed for free in their DM.
|
||||||
|
"""
|
||||||
|
@functools.wraps(handler)
|
||||||
|
async def wrapped(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
user = update.effective_user
|
||||||
|
chat = update.effective_chat
|
||||||
|
uid = user.id if user else None
|
||||||
|
if OWNER_ID is not None and (uid == OWNER_ID or (chat and chat.id == OWNER_ID)):
|
||||||
|
return await handler(update, context)
|
||||||
|
logger.warning(
|
||||||
|
f"Unauthorized access blocked: user={uid} "
|
||||||
|
f"chat={chat.id if chat else None} handler={handler.__name__}"
|
||||||
|
)
|
||||||
|
if update.callback_query:
|
||||||
|
await update.callback_query.answer("🔒 Not authorized.")
|
||||||
|
elif update.message:
|
||||||
|
await update.message.reply_text(
|
||||||
|
"🔒 This is a private control bot. Signals are published "
|
||||||
|
"in the subscriber channel."
|
||||||
|
)
|
||||||
|
return wrapped
|
||||||
|
|
||||||
|
|
||||||
|
@_owner_only
|
||||||
async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
scanning = "running" if _autoscan_chats else "off"
|
scanning = "running" if _autoscan_chats else "off"
|
||||||
text = (
|
text = (
|
||||||
@@ -788,10 +865,14 @@ _HELP_TEXT = (
|
|||||||
"*Auto-scan* fires at candle close: M5/M15/H1/H4.\n"
|
"*Auto-scan* fires at candle close: M5/M15/H1/H4.\n"
|
||||||
"*Alerts* button sets the minimum quality grade that gets sent.\n"
|
"*Alerts* button sets the minimum quality grade that gets sent.\n"
|
||||||
"*WebSocket* streams live prices; key-level breaches trigger an "
|
"*WebSocket* streams live prices; key-level breaches trigger an "
|
||||||
"instant M15 rescan and alert (5-min cooldown)."
|
"instant M15 rescan and alert (5-min cooldown).\n"
|
||||||
|
"*Channel feed*: signal + breach alerts mirror to the subscriber "
|
||||||
|
"channel when TELEGRAM_CHANNEL_ID is set; /autoscan only toggles "
|
||||||
|
"your DM copy."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@_owner_only
|
||||||
async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
await update.message.reply_text(_HELP_TEXT, parse_mode=ParseMode.MARKDOWN)
|
await update.message.reply_text(_HELP_TEXT, parse_mode=ParseMode.MARKDOWN)
|
||||||
|
|
||||||
@@ -806,6 +887,9 @@ def _status_text() -> str:
|
|||||||
else:
|
else:
|
||||||
lines.append("⏹ Auto-scan: *stopped*")
|
lines.append("⏹ Auto-scan: *stopped*")
|
||||||
|
|
||||||
|
if CHANNEL_ID is not None:
|
||||||
|
lines.append(f"📢 Channel feed: *on* (`{CHANNEL_ID}`)")
|
||||||
|
|
||||||
if _ws_task and not _ws_task.done():
|
if _ws_task and not _ws_task.done():
|
||||||
lines.append("📡 WebSocket: *connected*")
|
lines.append("📡 WebSocket: *connected*")
|
||||||
else:
|
else:
|
||||||
@@ -848,10 +932,12 @@ def _status_text() -> str:
|
|||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
@_owner_only
|
||||||
async def cmd_status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def cmd_status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
await update.message.reply_text(_status_text(), parse_mode=ParseMode.MARKDOWN)
|
await update.message.reply_text(_status_text(), parse_mode=ParseMode.MARKDOWN)
|
||||||
|
|
||||||
|
|
||||||
|
@_owner_only
|
||||||
async def cmd_cache(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def cmd_cache(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
dp = _get_shared_dp()
|
dp = _get_shared_dp()
|
||||||
if not dp._cache_timestamps:
|
if not dp._cache_timestamps:
|
||||||
@@ -881,6 +967,7 @@ async def cmd_cache(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|||||||
await update.message.reply_text("\n".join(lines), parse_mode=ParseMode.MARKDOWN)
|
await update.message.reply_text("\n".join(lines), parse_mode=ParseMode.MARKDOWN)
|
||||||
|
|
||||||
|
|
||||||
|
@_owner_only
|
||||||
async def cmd_clearcache(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def cmd_clearcache(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
dp = _get_shared_dp()
|
dp = _get_shared_dp()
|
||||||
count = dp.clear_cache()
|
count = dp.clear_cache()
|
||||||
@@ -890,17 +977,19 @@ async def cmd_clearcache(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@_owner_only
|
||||||
async def cmd_autoscan(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def cmd_autoscan(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
chat_id = update.effective_chat.id
|
chat_id = update.effective_chat.id
|
||||||
|
|
||||||
if chat_id in _autoscan_chats:
|
if chat_id in _autoscan_chats:
|
||||||
_autoscan_chats.discard(chat_id)
|
_autoscan_chats.discard(chat_id)
|
||||||
if not _autoscan_chats:
|
if not _feed_active():
|
||||||
_stop_scheduler()
|
_stop_scheduler()
|
||||||
await update.message.reply_text(
|
msg = "⏹ Auto-scan stopped."
|
||||||
"⏹ Auto-scan stopped.",
|
else:
|
||||||
parse_mode=ParseMode.MARKDOWN,
|
msg = ("⏹ Auto-scan stopped for this chat.\n"
|
||||||
)
|
"📢 Subscriber channel feed continues.")
|
||||||
|
await update.message.reply_text(msg, parse_mode=ParseMode.MARKDOWN)
|
||||||
return
|
return
|
||||||
|
|
||||||
_autoscan_chats.add(chat_id)
|
_autoscan_chats.add(chat_id)
|
||||||
@@ -921,6 +1010,7 @@ async def cmd_autoscan(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@_owner_only
|
||||||
async def cmd_scan(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def cmd_scan(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
await update.message.reply_text(
|
await update.message.reply_text(
|
||||||
f"🔍 *{_PAIRS_LABEL} — Pick a timeframe*",
|
f"🔍 *{_PAIRS_LABEL} — Pick a timeframe*",
|
||||||
@@ -929,6 +1019,7 @@ async def cmd_scan(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@_owner_only
|
||||||
async def cb_scan(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def cb_scan(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
query = update.callback_query
|
query = update.callback_query
|
||||||
await query.answer()
|
await query.answer()
|
||||||
@@ -954,6 +1045,7 @@ async def cb_scan(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|||||||
await _safe_edit(query, text, reply_markup=main_menu_keyboard())
|
await _safe_edit(query, text, reply_markup=main_menu_keyboard())
|
||||||
|
|
||||||
|
|
||||||
|
@_owner_only
|
||||||
async def cb_menu(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def cb_menu(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
"""Interactive menu actions: autoscan toggle, quality cycle, status, help."""
|
"""Interactive menu actions: autoscan toggle, quality cycle, status, help."""
|
||||||
global _alert_min_quality
|
global _alert_min_quality
|
||||||
@@ -964,9 +1056,11 @@ async def cb_menu(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|||||||
chat_id = query.message.chat.id
|
chat_id = query.message.chat.id
|
||||||
if chat_id in _autoscan_chats:
|
if chat_id in _autoscan_chats:
|
||||||
_autoscan_chats.discard(chat_id)
|
_autoscan_chats.discard(chat_id)
|
||||||
if not _autoscan_chats:
|
if not _feed_active():
|
||||||
_stop_scheduler()
|
_stop_scheduler()
|
||||||
await query.answer("Auto-scan stopped")
|
await query.answer("Auto-scan stopped")
|
||||||
|
else:
|
||||||
|
await query.answer("DM alerts off — channel feed continues")
|
||||||
else:
|
else:
|
||||||
_autoscan_chats.add(chat_id)
|
_autoscan_chats.add(chat_id)
|
||||||
_start_scheduler(context.bot)
|
_start_scheduler(context.bot)
|
||||||
@@ -1044,6 +1138,7 @@ async def _cb_scan_all(query, symbol: str = XAUUSD) -> None:
|
|||||||
lines.append(err)
|
lines.append(err)
|
||||||
|
|
||||||
lines.append("_Use buttons below for detailed single-TF view._")
|
lines.append("_Use buttons below for detailed single-TF view._")
|
||||||
|
lines += ["", _DISCLAIMER]
|
||||||
|
|
||||||
await _safe_edit(query, "\n".join(lines), reply_markup=main_menu_keyboard())
|
await _safe_edit(query, "\n".join(lines), reply_markup=main_menu_keyboard())
|
||||||
|
|
||||||
@@ -1062,13 +1157,16 @@ async def _post_init(app: Application) -> None:
|
|||||||
("help", "All commands"),
|
("help", "All commands"),
|
||||||
])
|
])
|
||||||
|
|
||||||
if not CHAT_ID:
|
if CHAT_ID:
|
||||||
logger.info("TELEGRAM_CHAT_ID not set — auto-scan disabled on boot. "
|
_autoscan_chats.add(int(CHAT_ID))
|
||||||
"Use /autoscan in chat to enable.")
|
|
||||||
|
if not _feed_active():
|
||||||
|
logger.info("Neither TELEGRAM_CHAT_ID nor TELEGRAM_CHANNEL_ID set — "
|
||||||
|
"auto-scan disabled on boot. Use /autoscan in chat to enable.")
|
||||||
return
|
return
|
||||||
|
|
||||||
chat_id = int(CHAT_ID)
|
if CHANNEL_ID is not None:
|
||||||
_autoscan_chats.add(chat_id)
|
logger.info(f"Subscriber channel feed enabled: {CHANNEL_ID}")
|
||||||
|
|
||||||
logger.info("Running initial scan to populate level cache and signal stats...")
|
logger.info("Running initial scan to populate level cache and signal stats...")
|
||||||
try:
|
try:
|
||||||
@@ -1094,23 +1192,28 @@ async def _post_init(app: Application) -> None:
|
|||||||
next_h1 = _seconds_until_boundary(60, buffer=10)
|
next_h1 = _seconds_until_boundary(60, buffer=10)
|
||||||
next_h4 = _seconds_until_boundary(240, buffer=10)
|
next_h4 = _seconds_until_boundary(240, buffer=10)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Auto-scan started for chat {chat_id} — "
|
f"Auto-scan started (chats={sorted(_autoscan_chats)}, channel={CHANNEL_ID}) — "
|
||||||
f"M5 {next_m5:.0f}s, M15 {next_m15:.0f}s, H1 {next_h1:.0f}s, H4 {next_h4:.0f}s"
|
f"M5 {next_m5:.0f}s, M15 {next_m15:.0f}s, H1 {next_h1:.0f}s, H4 {next_h4:.0f}s"
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
# Boot message goes to the owner DM only — the subscriber channel
|
||||||
await app.bot.send_message(
|
# receives nothing but signal and breach alerts.
|
||||||
chat_id,
|
if OWNER_ID is not None:
|
||||||
f"🤖 *ForexBot2 started*\n\n"
|
channel_line = "📢 Subscriber channel feed active.\n" if CHANNEL_ID else ""
|
||||||
f"Auto-scanning {_PAIRS_LABEL} on M5/M15/H1/H4.\n"
|
try:
|
||||||
f"📡 Live price monitoring active.\n"
|
await app.bot.send_message(
|
||||||
f"Next: M5 {next_m5 / 60:.0f}m | M15 {next_m15 / 60:.0f}m | "
|
OWNER_ID,
|
||||||
f"H1 {next_h1 / 60:.0f}m | H4 {next_h4 / 60:.0f}m\n\n"
|
f"🤖 *ForexBot2 started*\n\n"
|
||||||
f"/autoscan to stop",
|
f"Auto-scanning {_PAIRS_LABEL} on M5/M15/H1/H4.\n"
|
||||||
parse_mode=ParseMode.MARKDOWN,
|
f"📡 Live price monitoring active.\n"
|
||||||
)
|
f"{channel_line}"
|
||||||
except Exception:
|
f"Next: M5 {next_m5 / 60:.0f}m | M15 {next_m15 / 60:.0f}m | "
|
||||||
logger.exception("Failed to send boot message")
|
f"H1 {next_h1 / 60:.0f}m | H4 {next_h4 / 60:.0f}m\n\n"
|
||||||
|
f"/autoscan to stop",
|
||||||
|
parse_mode=ParseMode.MARKDOWN,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to send boot message")
|
||||||
|
|
||||||
|
|
||||||
# ── Graceful shutdown ─────────────────────────────────────────────────
|
# ── Graceful shutdown ─────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user