Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 56 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@

# banini-tracker

追蹤「股海冥燈」巴逆逆(8zz)的 Facebook 社群貼文,透過 Apify 抓取、AI 反指標分析、Telegram 即時推送。
追蹤「股海冥燈」巴逆逆(8zz)的 Facebook 社群貼文,透過 Apify 抓取、AI 反指標分析、Telegram 即時推送,並自動追蹤預測準確度

- 辨識她提到的標的(個股、ETF、原物料)
- 判斷她的操作(買入 / 被套 / 停損)
- 反轉推導(她停損 → 可能反彈、她買入 → 可能下跌)
- 推導連鎖效應(油價跌 → 製造業利多 → 電子股受惠)
- 自動記錄預測,追蹤 5 個交易日的實際走勢

> **Claude Code 使用者?** 直接把 [`skill/SKILL.md`](skill/SKILL.md) 加到你的 `.claude/skills/` 就能用。Claude 自己當分析引擎,不需要額外 LLM。

支援兩種使用模式:
- **常駐排程**:Docker 部署,自動盤中/盤後排程 + LLM 分析 + Telegram 推送
- **常駐排程**:Docker 部署,自動盤中/盤後排程 + LLM 分析 + Telegram 推送 + 預測追蹤
- **CLI 工具**:`npx @cablate/banini-tracker`,搭配 Claude Code 等 AI 手動執行分析

## 快速開始(常駐排程)
Expand All @@ -26,26 +27,32 @@ cp .env.example .env

# 2. Docker 部署
docker build -t banini-tracker .
docker run -d --name banini --env-file .env banini-tracker
docker run -d --name banini --env-file .env -v banini-data:/data banini-tracker

# 3. 或本地直接跑
npm install && npm run start
```

### 排程規則

- **盤中**(週一~五 09:00-13:30):每 30 分鐘,FB only 抓 1 篇
- **盤後**(每天 23:00):FB 3 篇
| 排程 | 時間 | 說明 |
|------|------|------|
| 早晨補漏 | 每天 08:00 | 抓前一晚 22:00 後的貼文(3 篇) |
| 盤中 | 週一~五 09:07-13:07 每 30 分 | 抓 08:30 後的貼文(1 篇) |
| 追蹤更新 | 週一~五 15:00 | 更新預測追蹤(收盤後抓 OHLC) |
| 盤後 | 每天 23:03 | 抓 13:30 後的貼文(3 篇) |

每個排程只抓自己時間窗口內的貼文,搭配 seen.json 去重,確保無死角且不重複。

### npm scripts

| 指令 | 說明 |
|------|------|
| `npm run start` | 常駐排程模式(盤中 + 盤後自動跑) |
| `npm run start` | 常駐排程模式(全部排程自動跑) |
| `npm run dev` | 單次執行(FB 3 篇) |
| `npm run dry` | 只抓取,不呼叫 LLM |
| `npm run market` | 盤中模式(FB only, 1 篇) |
| `npm run evening` | 盤後模式( 3 篇) |
| `npm run market` | 盤中模式(FB 1 篇) |
| `npm run evening` | 盤後模式(FB 3 篇) |

### .env 設定

Expand All @@ -60,8 +67,37 @@ TG_CHANNEL_ID=-100...
# 影片轉錄(選填,啟用後自動轉錄影片貼文)
TRANSCRIBER=groq
GROQ_API_KEY=gsk_...

# FinMind API(選填,免費可用,註冊可提高額度)
FINMIND_TOKEN=...

# 資料目錄(Docker 建議掛載 /data)
DATA_DIR=/data
```

## 預測追蹤系統

LLM 分析出標的後,系統自動:

1. **映射股票代碼**:台股名稱 → 代碼(2230 檔上市 + 上櫃)
2. **記錄基準價格**:以貼文發佈時間查對應交易日收盤價
3. **追蹤 5 個交易日**:每天 15:00 收盤後抓 OHLC,記錄漲跌幅
4. **同股票取代**:新預測自動取代同標的舊預測(supersede 機制)

勝敗判定在查詢時決定,支援多維度分析(不同持有天數、信心度分群、操作類型)。

### 資料儲存

使用 SQLite(better-sqlite3),資料表:

| 表 | 用途 |
|----|------|
| `posts` | 所有貼文原文(即時 + 歷史回測統一來源) |
| `predictions` | 預測記錄(標的、方向、基準價、狀態) |
| `price_snapshots` | 每日 OHLC 快照(5 天追蹤期) |

資料庫位置:`$DATA_DIR/banini.db`(Docker 掛載 `/data`,本地 `~/.banini-tracker/`)

## CLI 工具模式

不需 clone repo,任何環境直接用:
Expand All @@ -76,8 +112,11 @@ npx @cablate/banini-tracker init \
# 抓取 Facebook 最新 3 篇
npx @cablate/banini-tracker fetch -s fb -n 3 --mark-seen

# 抓取指定日期區間(回測用)
npx @cablate/banini-tracker fetch --since 2025-04-01 --until 2025-05-01 -n 100

# 推送結果到 Telegram
npx @cablate/banini-tracker push -m "分析結果..."
npx @cablate/banini-tracker push -f report.txt
```

### CLI 指令
Expand All @@ -97,6 +136,8 @@ npx @cablate/banini-tracker push -m "分析結果..."
```
-s, --source <source> 來源:fb(預設 fb)
-n, --limit <n> 每個來源抓幾篇(預設 3)
--since <date> 只抓此時間之後的貼文(YYYY-MM-DD / ISO 時間戳 / 相對時間如 "2 months")
--until <date> 只抓此時間之前的貼文
--no-dedup 不去重
--mark-seen 輸出後自動標記已讀
```
Expand All @@ -105,7 +146,7 @@ npx @cablate/banini-tracker push -m "分析結果..."

```
-m, --message <text> 直接帶訊息
-f, --file <path> 從檔案讀取
-f, --file <path> 從檔案讀取(推薦多行內容用這個)
--parse-mode <mode> HTML / Markdown / none(預設 HTML)
```

Expand All @@ -117,22 +158,22 @@ npx @cablate/banini-tracker push -m "分析結果..."

1. `fetch` 抓貼文 → Claude 讀 JSON
2. Claude 分析 + WebSearch 查最新走勢
3. Claude 組報告 → `push` 推送 Telegram
3. Claude 組報告 → `push -f` 推送 Telegram

詳見 [`skill/SKILL.md`](skill/SKILL.md)。

## 費用估算

| 項目 | 單次費用 | 頻率 | 月估算 |
|------|---------|------|--------|
| Facebook 抓取(Apify) | ~$0.02 | 盤中 ~198 次 + 盤後 30 次 | ~$4.56 |
| Facebook 抓取(Apify) | ~$0.005/篇 | ~270 篇/月 | ~$1.35 |
| LLM 分析(常駐模式) | 依模型而定 | 同上 | 依模型定價 |
| 影片轉錄(Groq Whisper) | ~$0.006/分鐘 | 視影片數量 | 極低 |
| 股價查詢(FinMind) | 免費 | 每日收盤後 | $0 |
| Telegram 推送 | 免費 | — | $0 |

> 盤中:週一~五 09:00-13:30 每 30 分鐘(~9 次/日 × 22 工作日)
> 盤後:每天 23:00(30 次/月)
> CLI 模式搭配 Claude Code 使用則不需 LLM 費用,Claude 自己分析
> CLI 模式搭配 Claude Code 使用不需 LLM 費用,Claude 自己分析。
> 回測歷史資料加日期篩選:~$7/千篇($5 基本 + $2 date filter add-on)。

## 為什麼只用 Facebook?

Expand Down
44 changes: 10 additions & 34 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,46 +312,22 @@ async function runInner(opts: RunOptions) {
console.log(`結果已存檔: ${outFile}`);
}

/**
* 產生台北時間今天指定時分的 ISO 時間戳
* 用於 Apify onlyPostsNewerThan 參數
*/
function taipeiToday(hours: number, minutes = 0): string {
const now = new Date();
const taipeiStr = now.toLocaleString('en-US', { timeZone: 'Asia/Taipei' });
const taipeiNow = new Date(taipeiStr);
taipeiNow.setHours(hours, minutes, 0, 0);
// 轉回 UTC:台北 = UTC+8
const utc = new Date(taipeiNow.getTime() - 8 * 60 * 60 * 1000);
return utc.toISOString();
}

function taipeiYesterday(hours: number, minutes = 0): string {
const now = new Date();
const taipeiStr = now.toLocaleString('en-US', { timeZone: 'Asia/Taipei' });
const taipeiNow = new Date(taipeiStr);
taipeiNow.setDate(taipeiNow.getDate() - 1);
taipeiNow.setHours(hours, minutes, 0, 0);
const utc = new Date(taipeiNow.getTime() - 8 * 60 * 60 * 1000);
return utc.toISOString();
}

// ── 入口 ────────────────────────────────────────────────────
if (isCronMode) {
// 早晨補漏:每天 08:00,抓前一晚 22:00 之後的貼文
// 早晨補漏:每天 08:00
cron.schedule('0 8 * * *', () => {
run({ maxPosts: 3, isDryRun: false, label: '早晨', since: taipeiYesterday(22, 0) })
run({ maxPosts: 3, isDryRun: false, label: '早晨' })
.catch((err) => console.error('[早晨] 執行失敗:', err));
}, { timezone: 'Asia/Taipei' });

// 盤中:週一到五 09:00-13:30,每 30 分鐘,抓 08:30 之後的貼文
// 盤中:週一到五 09:00-13:30,每 30 分鐘
cron.schedule('7,37 9-12 * * 1-5', () => {
run({ maxPosts: 1, isDryRun: false, label: '盤中', since: taipeiToday(8, 30) })
run({ maxPosts: 1, isDryRun: false, label: '盤中' })
.catch((err) => console.error('[盤中] 執行失敗:', err));
}, { timezone: 'Asia/Taipei' });

cron.schedule('7 13 * * 1-5', () => {
run({ maxPosts: 1, isDryRun: false, label: '盤中', since: taipeiToday(8, 30) })
run({ maxPosts: 1, isDryRun: false, label: '盤中' })
.catch((err) => console.error('[盤中] 執行失敗:', err));
}, { timezone: 'Asia/Taipei' });

Expand All @@ -361,17 +337,17 @@ if (isCronMode) {
.catch((err) => console.error('[追蹤更新] 執行失敗:', err));
}, { timezone: 'Asia/Taipei' });

// 盤後:每天晚上 23:03,抓 13:30 之後的貼文
// 盤後:每天晚上 23:03
cron.schedule('3 23 * * *', () => {
run({ maxPosts: 3, isDryRun: false, label: '盤後', since: taipeiToday(13, 30) })
run({ maxPosts: 3, isDryRun: false, label: '盤後' })
.catch((err) => console.error('[盤後] 執行失敗:', err));
}, { timezone: 'Asia/Taipei' });

console.log('=== 巴逆逆排程已啟動 ===');
console.log(' 早晨:每天 08:00(前晚 22:00 起,3 篇)');
console.log(' 盤中:週一~五 09:07/09:37/10:07/.../13:07(08:30 起,1 篇)');
console.log(' 早晨:每天 08:00(3 篇)');
console.log(' 盤中:週一~五 09:07/09:37/10:07/.../13:07(1 篇)');
console.log(' 追蹤更新:週一~五 15:00(預測追蹤判定)');
console.log(' 盤後:每天 23:03(13:30 起,3 篇)');
console.log(' 盤後:每天 23:03(3 篇)');
console.log(' 按 Ctrl+C 停止\n');

} else {
Expand Down
Loading