今天是第二十四天!昨天我們成功導入了「台股加權/櫃買大盤模組與市場多空溫度計(/market)」,讓我們在操作個股前能先掌握全市場的資金風向。
不過,一個合格的自動化看盤機器人,不應該每次都需要我們手動去敲指令(如 /market 或 /watchlist)。最理想的情境是:每天下午台股收盤、籌碼資料結算後(例如 14:30),機器人自動抓取最新大盤溫度計與你個人資料庫裡的自選股清單,自動組合出一份完整的「盤後日報」推播到你的 Telegram!
今天我們的目標,就是將之前的 APScheduler 定時排程器與 SQLite 資料庫中的自選股清單連結起來,實現「全自動化大盤 + 自選股盤後巡檢推播」。
步驟一:先釐清邏輯
自動化盤後推播的運作流程:
import os
import sqlite3
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import ContextTypes, CommandHandler, CallbackQueryHandler
from fetcher import get_stock_data
from chart import create_stock_chart
from database import add_to_watchlist, remove_from_watchlist, get_watchlist, DB_NAME
from market import get_market_overview
def get_stock_keyboard(symbol):
keyboard = [
[
InlineKeyboardButton("技術線圖", callback_data=f"chart_{symbol}"),
InlineKeyboardButton("籌碼動向", callback_data=f"chip_{symbol}")
],
[
InlineKeyboardButton("營收數據", callback_data=f"rev_{symbol}"),
InlineKeyboardButton("即時新聞", callback_data=f"news_{symbol}")
]
]
return InlineKeyboardMarkup(keyboard)
# 取得所有有設定自選股的 Unique Chat ID
def get_all_watchlist_chat_ids():
conn = sqlite3.connect(DB_NAME)
cursor = conn.cursor()
cursor.execute("SELECT DISTINCT chat_id FROM watchlist")
rows = cursor.fetchall()
conn.close()
return [row[0] for row in rows]
# 每日定時自動推播任務
async def scheduled_job(app):
print("[SCHEDULER] 開始執行每日盤後自動推播任務...")
# 1. 抓取最新大盤報告
market_report = get_market_overview()
if not market_report:
market_report = "[NOTICE] 無法取得最新大盤數據。"
# 2. 取得所有使用自選股功能的使用者
chat_ids = get_all_watchlist_chat_ids()
if not chat_ids:
print("[SCHEDULER] 目前資料庫中沒有任何使用者設定自選股。")
return
# 3. 針對每個使用者進行個別化推播
for chat_id in chat_ids:
stocks = get_watchlist(chat_id)
if not stocks:
continue
# 先推播大盤溫度計
await app.bot.send_message(
chat_id=chat_id,
text=f"-----------------------------------\n[每日盤後自動推播 - 大盤總覽]\n-----------------------------------\n\n{market_report}"
)
# 彙整自選股個股簡報
watchlist_summary = "【今日自選股巡檢簡報】\n-----------------------------------\n"
for sym in stocks:
symbol, df, chip_data, revenue_data, news_data = get_stock_data(sym)
if df is None:
watchlist_summary += f"{sym}: 資料獲取失敗\n"
continue
latest = df.iloc[-1]
prev = df.iloc[-2]
change_pct = ((latest['Close'] - prev['Close']) / prev['Close']) * 100
watchlist_summary += f"{symbol} | 收盤: {latest['Close']:.2f} ({change_pct:+.2f}%)\n"
# 抓取關鍵訊號
signals = []
if revenue_data and revenue_data.get('yoy', 0) >= 30:
signals.append("營收YoY>30%")
if chip_data and chip_data.get('foreign', 0) > 0 and chip_data.get('investment', 0) > 0:
signals.append("外資投信同買")
if latest['MA5'] > latest['MA20'] and prev['MA5'] <= prev['MA20']:
signals.append("5MA金叉20MA")
if signals:
watchlist_summary += " 訊號: " + ", ".join(signals) + "\n"
else:
watchlist_summary += " 訊號: 走勢平穩無特殊訊號\n"
watchlist_summary += "-----------------------------------\n"
# 發送自選股簡報
await app.bot.send_message(
chat_id=chat_id,
text=watchlist_summary
)
print("[SCHEDULER] 每日盤後自動推播任務發送完成!")
# 指令處理函式
async def market_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text("[INFO] 正在分析台股大盤最新數據,請稍後...")
report = get_market_overview()
if report:
await update.message.reply_text(report)
else:
await update.message.reply_text("[ERROR] 暫時無法獲取大盤指數數據。")
async def add_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not context.args:
await update.message.reply_text("請輸入股票代碼,例如:/add 2330")
return
raw_symbol = context.args[0].upper()
chat_id = update.effective_chat.id
if not raw_symbol.endswith('.TW') and not raw_symbol.endswith('.TWO'):
raw_symbol += '.TW'
success = add_to_watchlist(chat_id, raw_symbol)
if success:
await update.message.reply_text(f"[SUCCESS] 已成功將 {raw_symbol} 加入自選股追蹤清單!")
else:
await update.message.reply_text(f"[NOTICE] {raw_symbol} 已經在你的自選股清單中了。")
async def del_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not context.args:
await update.message.reply_text("請輸入股票代碼,例如:/del 2330")
return
raw_symbol = context.args[0].upper()
chat_id = update.effective_chat.id
if not raw_symbol.endswith('.TW') and not raw_symbol.endswith('.TWO'):
raw_symbol += '.TW'
success = remove_from_watchlist(chat_id, raw_symbol)
if success:
await update.message.reply_text(f"[SUCCESS] 已將 {raw_symbol} 從自選股清單中移除。")
else:
await update.message.reply_text(f"[NOTICE] 在自選股清單中找不到 {raw_symbol}。")
async def watchlist_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
stocks = get_watchlist(chat_id)
if not stocks:
await update.message.reply_text("目前自選股清單是空的!請使用 /add <代碼> 新增標的。")
return
text = "【我的口袋自選股清單】\n-----------------------------------\n"
keyboard = []
for idx, sym in enumerate(stocks, 1):
text += f"{idx}. {sym}\n"
keyboard.append([InlineKeyboardButton(f"看盤 {sym}", callback_data=f"view_{sym}")])
text += "-----------------------------------\n提示:可使用 /del <代碼> 移除標的。"
await update.message.reply_text(
text=text,
reply_markup=InlineKeyboardMarkup(keyboard)
)
async def stock_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not context.args:
await update.message.reply_text("請輸入股票代碼,例如:/stock 2330")
return
raw_symbol = context.args[0]
await update.message.reply_text(f"[INFO] 正在獲取 {raw_symbol} 完整資料,請稍後...")
symbol, df, chip_data, revenue_data, news_data = get_stock_data(raw_symbol)
if df is None:
await update.message.reply_text(f"[ERROR] 找不到股票代碼:{raw_symbol}")
return
latest = df.iloc[-1]
summary_text = (
f"【{symbol} 個股儀表板】\n"
f"日期:{df.index[-1].strftime('%Y-%m-%d')}\n"
f"最新收盤價:{latest['Close']:.2f} 元\n\n"
f"請選擇下方按鈕切換詳細看盤維度:"
)
await update.message.reply_text(
text=summary_text,
reply_markup=get_stock_keyboard(symbol)
)
async def button_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
data = query.data
action, symbol = data.split("_", 1)
if action == "view":
_, df, chip_data, revenue_data, news_data = get_stock_data(symbol)
latest = df.iloc[-1]
summary_text = (
f"【{symbol} 個股儀表板】\n"
f"日期:{df.index[-1].strftime('%Y-%m-%d')}\n"
f"最新收盤價:{latest['Close']:.2f} 元\n\n"
f"請選擇下方按鈕切換詳細看盤維度:"
)
await query.message.reply_text(
text=summary_text,
reply_markup=get_stock_keyboard(symbol)
)
return
_, df, chip_data, revenue_data, news_data = get_stock_data(symbol)
if action == "chart":
img_path = create_stock_chart(symbol, df)
with open(img_path, 'rb') as photo:
await query.message.reply_photo(
photo=photo,
caption=f"【{symbol} K線與技術指標線圖】",
reply_markup=get_stock_keyboard(symbol)
)
if os.path.exists(img_path):
os.remove(img_path)
elif action == "chip":
text = (
f"【{symbol} 三大法人籌碼動向】(單位: 張)\n"
f"-----------------------------------\n"
f"外資:{chip_data['foreign']:+d}\n"
f"投信:{chip_data['investment']:+d}\n"
f"自營商:{chip_data['dealer']:+d}\n"
f"-----------------------------------\n"
f"法人合計:{chip_data['total']:+d}\n"
)
await query.message.reply_text(text=text, reply_markup=get_stock_keyboard(symbol))
elif action == "rev":
text = (
f"【{symbol} 最新月營收報告】({revenue_data['month']})\n"
f"-----------------------------------\n"
f"單月營收:{revenue_data['revenue'] / 100000:.2f} 億元\n"
f"MoM (月增率):{revenue_data['mom']:+.1f}%\n"
f"YoY (年增率):{revenue_data['yoy']:+.1f}%\n"
)
await query.message.reply_text(text=text, reply_markup=get_stock_keyboard(symbol))
elif action == "news":
text = f"【{symbol} 即時消息面分析】({news_data['sentiment']})\n-----------------------------------\n"
for idx, item in enumerate(news_data["list"], 1):
text += f"{idx}. {item['title']}\n"
await query.message.reply_text(text=text, reply_markup=get_stock_keyboard(symbol))
def setup_handlers(app):
app.add_handler(CommandHandler("start", lambda u, c: u.message.reply_text("歡迎使用台股看盤小幫手!")))
app.add_handler(CommandHandler("stock", stock_command))
app.add_handler(CommandHandler("add", add_command))
app.add_handler(CommandHandler("del", del_command))
app.add_handler(CommandHandler("watchlist", watchlist_command))
app.add_handler(CommandHandler("market", market_command))
app.add_handler(CallbackQueryHandler(button_callback))
解釋一下程式碼吧!
動態多使用者廣播支援:
透由 SELECT DISTINCT chat_id FROM watchlist,無論有多少位使用者使用了你的 Bot 並新增自選股,排程器都會在 14:30 依次發送個人專屬的自選股簡報,擴充性很好。
濃縮式訊號簡報:
自動推播不採用單檔大量圖文洗版的形式,而是將所有自選股的「最新漲跌幅」與「關鍵訊號」精簡濃縮在同一封訊息中,讓使用者 5 秒內就能完成口袋名單的盤後掃描。
步驟三:測試推播效果
若想要立刻測試排程發送效果(不用等到 14:30),可以在 main.py 的 if name == "main": 下方臨時加入測試代碼,或是直接在手動呼叫時觸發。
小結一下:
告別了手動查詢,現在每天收盤後,Telegram 就會自動跳出今日大盤溫度計與你口袋名單的健康檢查報告。
今天我們成功整合了定時排程、SQLite 自選股清單與大盤模組,完成了「全自動盤後推播系統」!