股票量化分析 SaaS 架構設計:從 Python 分析引擎到訂閱制服務
分享如何將個人用的股票量化分析工具轉化為 SaaS 服務,涵蓋多投資大師評分模型、Python Worker + BullMQ 任務佇列、NestJS API 設計與 Lemon Squeezy 金流整合。
從個人工具到 SaaS 服務
我原本有一個私人使用的 Python 腳本(叫做 buffet),每天自動拉取台股和美股的財報數據,用多位投資大師的選股標準做量化評分,然後產出一份 HTML 報告。
用了半年後發現一個問題:這個工具對我很有用,但要分享給朋友用很困難。他們得自己設定 Python 環境、申請 API key、跑 cron job。於是想法產生了——把它做成一個 Web 服務,讓任何人都能用。
核心價值:多投資大師評分模型
系統的核心邏輯是用多個投資大師的選股框架,對每支股票做量化評分:
巴菲特評分
- ROE 持續大於 15%
- 毛利率穩定或成長
- 負債權益比合理
- 自由現金流為正
- 本益比低於產業平均
葛拉漢評分(安全邊際)
- 股價低於淨值 × 1.5
- 本益比低於 15
- 流動比率大於 2
- 近 5 年未虧損
彼得林區評分(成長股)
- PEG ratio 小於 1
- 營收年增率大於 15%
- 盈餘成長穩定
費雪評分(質化面)
- 研發費用占比
- 營收多元化程度
- 管理階層持股變化
每個面向 0-100 分,加權後得出綜合評分。再搭配護城河分析、新聞風險評估、退出條件判斷。
系統架構
[Browser] → [Next.js Frontend :3000]
│
▼
[NestJS API :4000]
│
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
[PostgreSQL] [Redis] [Lemon Squeezy]
│
▼
[BullMQ Queue]
│
▼
[Python Worker]
(財務分析)
為什麼分成 Node.js API + Python Worker?
兩個原因:
- 生態系:財務分析用到 pandas、numpy、yfinance 等 Python 生態系的工具,用 Node.js 重寫不划算
- 解耦:分析引擎可能跑 30 分鐘(一次分析 70 支股票),不能阻塞 API server
用 BullMQ 做中間的橋樑:NestJS 排程觸發任務 → Redis Queue → Python Worker 消費並執行分析。
BullMQ:跨語言的任務佇列
NestJS 端(發布任務)
// 每日凌晨 2:00 觸發全量分析
@Cron('0 2 * * *')
async triggerDailyAnalysis() {
const stocks = await this.stockService.getTrackedStocks();
for (const stock of stocks) {
await this.analysisQueue.add('analyze-stock', {
symbol: stock.symbol,
market: stock.market, // 'tw' | 'us'
}, {
attempts: 3,
backoff: { type: 'exponential', delay: 5000 },
});
}
this.logger.log(`Queued ${stocks.length} stocks for analysis`);
}
Python 端(消費任務)
from bullmq import Worker, Job
import asyncio
async def process_stock(job: Job):
symbol = job.data['symbol']
market = job.data['market']
# 拉取財報數據
financials = fetch_financials(symbol, market)
# 多大師評分
scores = {
'buffett': calculate_buffett_score(financials),
'graham': calculate_graham_score(financials),
'lynch': calculate_lynch_score(financials),
'fisher': calculate_fisher_score(financials),
}
# 護城河分析
moat = analyze_moat(financials)
# 寫入資料庫
save_analysis_result(symbol, scores, moat)
return {'symbol': symbol, 'overall_score': weighted_average(scores)}
worker = Worker('analysis', process_stock, {
'connection': {'host': 'redis', 'port': 6379}
})
為什麼用 BullMQ 而不是 Celery?
- BullMQ 用 Redis(系統已經有 Redis)
- NestJS 有官方的
@nestjs/bullmq整合 - Python 端的
bullmq套件可以直接消費同一個 queue - 不需要額外的 broker(Celery 需要 RabbitMQ 或 Redis + 額外設定)
訂閱制與付費牆
Lemon Squeezy 整合
選擇 Lemon Squeezy 而非 Stripe 的原因:
- 台灣開發者不需要自己處理稅務(Lemon Squeezy 是 Merchant of Record)
- 設定比 Stripe 簡單
- 支援訂閱制
Webhook 驅動的訂閱狀態
@Post('webhook/lemon-squeezy')
async handleWebhook(@Req() req: Request, @Body() body: any) {
// 1. 驗證 webhook 簽名
const signature = req.headers['x-signature'];
if (!this.verifySignature(body, signature)) {
throw new UnauthorizedException('Invalid signature');
}
// 2. 根據事件類型更新訂閱狀態
const event = body.meta.event_name;
switch (event) {
case 'subscription_created':
await this.activateSubscription(body.data);
break;
case 'subscription_expired':
await this.deactivateSubscription(body.data);
break;
case 'subscription_payment_failed':
await this.handlePaymentFailure(body.data);
break;
}
}
付費牆邏輯
免費用戶可以看到股票列表和綜合評分,但詳細的各大師分項評分、護城河分析、歷史趨勢圖表需要訂閱。
// API Guard
@UseGuards(SubscriptionGuard)
@Get('stocks/:symbol/detailed-analysis')
async getDetailedAnalysis(@Param('symbol') symbol: string) {
return this.analysisService.getDetailed(symbol);
}
前端 Dashboard 設計
股票列表頁
- 依評分排序的股票卡片
- 快速篩選:市場(台股/美股)、產業、評分區間
- 搜尋功能
個股詳情頁
- 雷達圖:各大師評分
- 時間序列:評分歷史趨勢
- 護城河分析(文字 + 標籤)
- 新聞風險指標
- 退出條件檢查清單
資料更新策略
每日批次 vs 即時
我們選擇每日批次更新而非即時串流,原因:
- 財報數據本質是低頻的:財報一季才更新一次,每日更新已經足夠
- API 成本:即時股價 API 很貴,而我們的評分模型不依賴即時價格
- 系統簡單度:批次處理的架構比即時串流簡單一個數量級
增量更新
不是每天都重算所有 70 支股票。只在以下情況才重算:
- 有新的財報季報出來
- 股價大幅變動(超過 10%)
- 使用者新追蹤了某支股票
其餘時間只更新價格相關的指標(本益比、股價淨值比等),不重跑整套分析。
多租戶設計
每個使用者有自己的追蹤清單,但分析結果是共享的(同一支股票的評分對所有使用者相同):
-- 共享的分析結果
stock_analyses (symbol, market, scores, moat, updated_at)
-- 使用者個人的追蹤清單
user_watchlists (user_id, symbol, added_at)
-- 使用者的訂閱狀態
subscriptions (user_id, plan, status, expires_at, lemon_squeezy_id)
這個設計讓分析引擎只需要算一次,結果所有使用者共用。
容器化部署
services:
stox-frontend:
build: ./apps/web
ports: ["3000:3000"]
stox-api:
build: ./apps/api
ports: ["4000:4000"]
depends_on:
postgres: { condition: service_healthy }
redis: { condition: service_healthy }
stox-worker:
build: ./worker
depends_on:
redis: { condition: service_healthy }
postgres: { condition: service_healthy }
postgres:
image: postgres:17
healthcheck:
test: ["CMD", "pg_isready"]
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
五個容器各司其職,Docker Compose 一鍵啟動整個環境。
學到的經驗
把驗證過的個人工具產品化
先在自己身上驗證需求(用了半年的 buffet 腳本),確認有價值後再投資做成產品。避免了「做了一個沒人要用的東西」的風險。
BullMQ 跨語言的坑
Python 的 bullmq 套件和 Node.js 版本在 job data 的序列化上偶爾有不一致。建議用 JSON-safe 的基本型別(string、number、boolean),不要傳 datetime 物件。
金流整合比想像中複雜
不是串好 webhook 就結束了。還需要處理:
- 訂閱到期的 grace period
- 付款失敗的重試通知
- 使用者取消後的資料保留策略
- 試用期到期的轉換流程
結語
把個人工具變成 SaaS 服務的核心挑戰不在功能本身——分析引擎已經寫好了——而在「非功能需求」:認證、付費、多租戶、可靠性、部署。這些「無聊」的工程工作往往佔了整個專案 60% 的時間,但它們決定了產品能否真正被其他人使用。
關於作者

GWP4 STUDIO 創辦人,超過 8 年軟體開發經驗,專精資料工程、全端開發與系統架構。 持續在部落格分享專案實作經驗與技術心得。