@cablate/banini-tracker 2.0.9 → 2.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,17 +4,18 @@
4
4
 
5
5
  # banini-tracker
6
6
 
7
- 追蹤「股海冥燈」巴逆逆(8zz)的 Facebook 社群貼文,透過 Apify 抓取、AI 反指標分析、Telegram 即時推送。
7
+ 追蹤「股海冥燈」巴逆逆(8zz)的 Facebook 社群貼文,透過 Apify 抓取、AI 反指標分析、Telegram 即時推送,並自動追蹤預測準確度。
8
8
 
9
9
  - 辨識她提到的標的(個股、ETF、原物料)
10
10
  - 判斷她的操作(買入 / 被套 / 停損)
11
11
  - 反轉推導(她停損 → 可能反彈、她買入 → 可能下跌)
12
12
  - 推導連鎖效應(油價跌 → 製造業利多 → 電子股受惠)
13
+ - 自動記錄預測,追蹤 5 個交易日的實際走勢
13
14
 
14
15
  > **Claude Code 使用者?** 直接把 [`skill/SKILL.md`](skill/SKILL.md) 加到你的 `.claude/skills/` 就能用。Claude 自己當分析引擎,不需要額外 LLM。
15
16
 
16
17
  支援兩種使用模式:
17
- - **常駐排程**:Docker 部署,自動盤中/盤後排程 + LLM 分析 + Telegram 推送
18
+ - **常駐排程**:Docker 部署,自動盤中/盤後排程 + LLM 分析 + Telegram 推送 + 預測追蹤
18
19
  - **CLI 工具**:`npx @cablate/banini-tracker`,搭配 Claude Code 等 AI 手動執行分析
19
20
 
20
21
  ## 快速開始(常駐排程)
@@ -26,7 +27,7 @@ cp .env.example .env
26
27
 
27
28
  # 2. Docker 部署
28
29
  docker build -t banini-tracker .
29
- docker run -d --name banini --env-file .env banini-tracker
30
+ docker run -d --name banini --env-file .env -v banini-data:/data banini-tracker
30
31
 
31
32
  # 3. 或本地直接跑
32
33
  npm install && npm run start
@@ -34,18 +35,24 @@ npm install && npm run start
34
35
 
35
36
  ### 排程規則
36
37
 
37
- - **盤中**(週一~五 09:00-13:30):每 30 分鐘,FB only 抓 1 篇
38
- - **盤後**(每天 23:00):FB 3 篇
38
+ | 排程 | 時間 | 說明 |
39
+ |------|------|------|
40
+ | 早晨補漏 | 每天 08:00 | 抓前一晚 22:00 後的貼文(3 篇) |
41
+ | 盤中 | 週一~五 09:07-13:07 每 30 分 | 抓 08:30 後的貼文(1 篇) |
42
+ | 追蹤更新 | 週一~五 15:00 | 更新預測追蹤(收盤後抓 OHLC) |
43
+ | 盤後 | 每天 23:03 | 抓 13:30 後的貼文(3 篇) |
44
+
45
+ 每個排程只抓自己時間窗口內的貼文,搭配 seen.json 去重,確保無死角且不重複。
39
46
 
40
47
  ### npm scripts
41
48
 
42
49
  | 指令 | 說明 |
43
50
  |------|------|
44
- | `npm run start` | 常駐排程模式(盤中 + 盤後自動跑) |
51
+ | `npm run start` | 常駐排程模式(全部排程自動跑) |
45
52
  | `npm run dev` | 單次執行(FB 3 篇) |
46
53
  | `npm run dry` | 只抓取,不呼叫 LLM |
47
- | `npm run market` | 盤中模式(FB only, 1 篇) |
48
- | `npm run evening` | 盤後模式(各 3 篇) |
54
+ | `npm run market` | 盤中模式(FB 1 篇) |
55
+ | `npm run evening` | 盤後模式(FB 3 篇) |
49
56
 
50
57
  ### .env 設定
51
58
 
@@ -60,8 +67,37 @@ TG_CHANNEL_ID=-100...
60
67
  # 影片轉錄(選填,啟用後自動轉錄影片貼文)
61
68
  TRANSCRIBER=groq
62
69
  GROQ_API_KEY=gsk_...
70
+
71
+ # FinMind API(選填,免費可用,註冊可提高額度)
72
+ FINMIND_TOKEN=...
73
+
74
+ # 資料目錄(Docker 建議掛載 /data)
75
+ DATA_DIR=/data
63
76
  ```
64
77
 
78
+ ## 預測追蹤系統
79
+
80
+ LLM 分析出標的後,系統自動:
81
+
82
+ 1. **映射股票代碼**:台股名稱 → 代碼(2230 檔上市 + 上櫃)
83
+ 2. **記錄基準價格**:以貼文發佈時間查對應交易日收盤價
84
+ 3. **追蹤 5 個交易日**:每天 15:00 收盤後抓 OHLC,記錄漲跌幅
85
+ 4. **同股票取代**:新預測自動取代同標的舊預測(supersede 機制)
86
+
87
+ 勝敗判定在查詢時決定,支援多維度分析(不同持有天數、信心度分群、操作類型)。
88
+
89
+ ### 資料儲存
90
+
91
+ 使用 SQLite(better-sqlite3),資料表:
92
+
93
+ | 表 | 用途 |
94
+ |----|------|
95
+ | `posts` | 所有貼文原文(即時 + 歷史回測統一來源) |
96
+ | `predictions` | 預測記錄(標的、方向、基準價、狀態) |
97
+ | `price_snapshots` | 每日 OHLC 快照(5 天追蹤期) |
98
+
99
+ 資料庫位置:`$DATA_DIR/banini.db`(Docker 掛載 `/data`,本地 `~/.banini-tracker/`)
100
+
65
101
  ## CLI 工具模式
66
102
 
67
103
  不需 clone repo,任何環境直接用:
@@ -76,8 +112,11 @@ npx @cablate/banini-tracker init \
76
112
  # 抓取 Facebook 最新 3 篇
77
113
  npx @cablate/banini-tracker fetch -s fb -n 3 --mark-seen
78
114
 
115
+ # 抓取指定日期區間(回測用)
116
+ npx @cablate/banini-tracker fetch --since 2025-04-01 --until 2025-05-01 -n 100
117
+
79
118
  # 推送結果到 Telegram
80
- npx @cablate/banini-tracker push -m "分析結果..."
119
+ npx @cablate/banini-tracker push -f report.txt
81
120
  ```
82
121
 
83
122
  ### CLI 指令
@@ -97,6 +136,8 @@ npx @cablate/banini-tracker push -m "分析結果..."
97
136
  ```
98
137
  -s, --source <source> 來源:fb(預設 fb)
99
138
  -n, --limit <n> 每個來源抓幾篇(預設 3)
139
+ --since <date> 只抓此時間之後的貼文(YYYY-MM-DD / ISO 時間戳 / 相對時間如 "2 months")
140
+ --until <date> 只抓此時間之前的貼文
100
141
  --no-dedup 不去重
101
142
  --mark-seen 輸出後自動標記已讀
102
143
  ```
@@ -105,7 +146,7 @@ npx @cablate/banini-tracker push -m "分析結果..."
105
146
 
106
147
  ```
107
148
  -m, --message <text> 直接帶訊息
108
- -f, --file <path> 從檔案讀取
149
+ -f, --file <path> 從檔案讀取(推薦多行內容用這個)
109
150
  --parse-mode <mode> HTML / Markdown / none(預設 HTML)
110
151
  ```
111
152
 
@@ -117,7 +158,7 @@ npx @cablate/banini-tracker push -m "分析結果..."
117
158
 
118
159
  1. `fetch` 抓貼文 → Claude 讀 JSON
119
160
  2. Claude 分析 + WebSearch 查最新走勢
120
- 3. Claude 組報告 → `push` 推送 Telegram
161
+ 3. Claude 組報告 → `push -f` 推送 Telegram
121
162
 
122
163
  詳見 [`skill/SKILL.md`](skill/SKILL.md)。
123
164
 
@@ -125,14 +166,14 @@ npx @cablate/banini-tracker push -m "分析結果..."
125
166
 
126
167
  | 項目 | 單次費用 | 頻率 | 月估算 |
127
168
  |------|---------|------|--------|
128
- | Facebook 抓取(Apify) | ~$0.02 | 盤中 ~198 + 盤後 30 次 | ~$4.56 |
169
+ | Facebook 抓取(Apify) | ~$0.005/篇 | ~270 篇/月 | ~$1.35 |
129
170
  | LLM 分析(常駐模式) | 依模型而定 | 同上 | 依模型定價 |
130
171
  | 影片轉錄(Groq Whisper) | ~$0.006/分鐘 | 視影片數量 | 極低 |
172
+ | 股價查詢(FinMind) | 免費 | 每日收盤後 | $0 |
131
173
  | Telegram 推送 | 免費 | — | $0 |
132
174
 
133
- > 盤中:週一~五 09:00-13:30 30 分鐘(~9 次/日 × 22 工作日)
134
- > 盤後:每天 23:00(30 次/月)
135
- > CLI 模式搭配 Claude Code 使用則不需 LLM 費用,Claude 自己分析
175
+ > CLI 模式搭配 Claude Code 使用不需 LLM 費用,Claude 自己分析。
176
+ > 回測歷史資料加日期篩選:~$7/千篇($5 基本 + $2 date filter add-on)。
136
177
 
137
178
  ## 為什麼只用 Facebook?
138
179
 
package/dist/cli.js CHANGED
@@ -62,6 +62,8 @@ program
62
62
  .description('抓取最新貼文(輸出 JSON 到 stdout)')
63
63
  .option('-s, --source <source>', '來源:fb', 'fb')
64
64
  .option('-n, --limit <n>', '每個來源抓幾篇', '3')
65
+ .option('--since <date>', '只抓此時間之後的貼文(YYYY-MM-DD 或 ISO 時間戳或相對時間如 "2 months")')
66
+ .option('--until <date>', '只抓此時間之前的貼文')
65
67
  .option('--no-dedup', '不做去重,抓到什麼就輸出什麼')
66
68
  .option('--mark-seen', '輸出後自動標記為已讀')
67
69
  .action(async (opts) => {
@@ -69,7 +71,8 @@ program
69
71
  const config = loadConfig();
70
72
  const limit = parseInt(opts.limit, 10);
71
73
  let posts = [];
72
- const fp = await fetchFacebookPosts(config.targets.facebookPageUrl, config.apifyToken, limit);
74
+ const fetchOpts = (opts.since || opts.until) ? { since: opts.since, until: opts.until } : undefined;
75
+ const fp = await fetchFacebookPosts(config.targets.facebookPageUrl, config.apifyToken, limit, fetchOpts);
73
76
  posts.push(...fp);
74
77
  // 按時間從新到舊
75
78
  posts.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
@@ -12,4 +12,8 @@ export interface FacebookPost {
12
12
  mediaType: string;
13
13
  mediaUrl: string;
14
14
  }
15
- export declare function fetchFacebookPosts(pageUrl: string, token: string, maxPosts?: number): Promise<FacebookPost[]>;
15
+ export interface FetchOptions {
16
+ since?: string;
17
+ until?: string;
18
+ }
19
+ export declare function fetchFacebookPosts(pageUrl: string, token: string, maxPosts?: number, options?: FetchOptions): Promise<FacebookPost[]>;
package/dist/facebook.js CHANGED
@@ -1,17 +1,22 @@
1
- export async function fetchFacebookPosts(pageUrl, token, maxPosts = 3) {
1
+ export async function fetchFacebookPosts(pageUrl, token, maxPosts = 3, options) {
2
2
  const actorId = 'apify~facebook-posts-scraper';
3
3
  const url = `https://api.apify.com/v2/acts/${actorId}/run-sync-get-dataset-items`;
4
+ const body = {
5
+ startUrls: [{ url: pageUrl }],
6
+ resultsLimit: maxPosts,
7
+ captionText: true,
8
+ };
9
+ if (options?.since)
10
+ body.onlyPostsNewerThan = options.since;
11
+ if (options?.until)
12
+ body.onlyPostsOlderThan = options.until;
4
13
  const res = await fetch(url, {
5
14
  method: 'POST',
6
15
  headers: {
7
16
  'Content-Type': 'application/json',
8
17
  Authorization: `Bearer ${token}`,
9
18
  },
10
- body: JSON.stringify({
11
- startUrls: [{ url: pageUrl }],
12
- resultsLimit: maxPosts,
13
- captionText: true,
14
- }),
19
+ body: JSON.stringify(body),
15
20
  signal: AbortSignal.timeout(180_000),
16
21
  });
17
22
  if (!res.ok) {
package/dist/index.js CHANGED
@@ -66,7 +66,8 @@ async function runInner(opts) {
66
66
  const allPosts = [];
67
67
  // 1. 抓取 Facebook(含 retry)
68
68
  try {
69
- const fbPosts = await withRetry(() => fetchFacebookPosts(FB_PAGE_URL, apifyToken, opts.maxPosts), { label: 'Facebook', maxRetries: 2, baseDelayMs: 5000 });
69
+ const fetchOpts = (opts.since || opts.until) ? { since: opts.since, until: opts.until } : undefined;
70
+ const fbPosts = await withRetry(() => fetchFacebookPosts(FB_PAGE_URL, apifyToken, opts.maxPosts, fetchOpts), { label: 'Facebook', maxRetries: 2, baseDelayMs: 5000 });
70
71
  allPosts.push(...fbPosts.map(fromFacebook));
71
72
  }
72
73
  catch (err) {
@@ -268,8 +269,12 @@ async function runInner(opts) {
268
269
  }
269
270
  // ── 入口 ────────────────────────────────────────────────────
270
271
  if (isCronMode) {
271
- // 盤中:週一到五 09:00-13:30,每 30 分鐘,FB only 抓 1 篇
272
- // cron 不支援半小時結束,用 9:00-13:00 30 + 13:30 單獨一個
272
+ // 早晨補漏:每天 08:00
273
+ cron.schedule('0 8 * * *', () => {
274
+ run({ maxPosts: 3, isDryRun: false, label: '早晨' })
275
+ .catch((err) => console.error('[早晨] 執行失敗:', err));
276
+ }, { timezone: 'Asia/Taipei' });
277
+ // 盤中:週一到五 09:00-13:30,每 30 分鐘
273
278
  cron.schedule('7,37 9-12 * * 1-5', () => {
274
279
  run({ maxPosts: 1, isDryRun: false, label: '盤中' })
275
280
  .catch((err) => console.error('[盤中] 執行失敗:', err));
@@ -283,15 +288,16 @@ if (isCronMode) {
283
288
  updateTracking()
284
289
  .catch((err) => console.error('[追蹤更新] 執行失敗:', err));
285
290
  }, { timezone: 'Asia/Taipei' });
286
- // 盤後:每天晚上 23:00,FB 3 篇
291
+ // 盤後:每天晚上 23:03
287
292
  cron.schedule('3 23 * * *', () => {
288
293
  run({ maxPosts: 3, isDryRun: false, label: '盤後' })
289
294
  .catch((err) => console.error('[盤後] 執行失敗:', err));
290
295
  }, { timezone: 'Asia/Taipei' });
291
296
  console.log('=== 巴逆逆排程已啟動 ===');
292
- console.log(' 盤中:週一~五 09:07/09:37/10:07/.../13:07FB, 1 篇)');
297
+ console.log(' 早晨:每天 08:003 篇)');
298
+ console.log(' 盤中:週一~五 09:07/09:37/10:07/.../13:07(1 篇)');
293
299
  console.log(' 追蹤更新:週一~五 15:00(預測追蹤判定)');
294
- console.log(' 盤後:每天 23:03(FB, 3 篇)');
300
+ console.log(' 盤後:每天 23:03(3 篇)');
295
301
  console.log(' 按 Ctrl+C 停止\n');
296
302
  }
297
303
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cablate/banini-tracker",
3
- "version": "2.0.9",
3
+ "version": "2.0.11",
4
4
  "description": "巴逆逆反指標追蹤器 — 常駐排程 + CLI 雙模式",
5
5
  "type": "module",
6
6
  "bin": {