@cablate/banini-tracker 2.0.0 → 2.0.1
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/LICENSE +21 -21
- package/README.md +133 -126
- package/dist/analyze.js +75 -75
- package/dist/index.js +26 -13
- package/dist/retry.d.ts +6 -0
- package/dist/retry.js +22 -0
- package/dist/telegram.d.ts +2 -0
- package/dist/telegram.js +16 -1
- package/package.json +46 -38
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 cablate
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 cablate
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,126 +1,133 @@
|
|
|
1
|
-
<p align="center">
|
|
2
|
-
<img src="assets/banner.svg" alt="banini-tracker banner" width="100%">
|
|
3
|
-
</p>
|
|
4
|
-
|
|
5
|
-
# banini-tracker
|
|
6
|
-
|
|
7
|
-
追蹤「股海冥燈」巴逆逆(8zz)的 Threads / Facebook 社群貼文,透過 Apify 抓取、AI 反指標分析、Telegram 即時推送。
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
-
|
|
97
|
-
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
##
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/banner.svg" alt="banini-tracker banner" width="100%">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# banini-tracker
|
|
6
|
+
|
|
7
|
+
追蹤「股海冥燈」巴逆逆(8zz)的 Threads / Facebook 社群貼文,透過 Apify 抓取、AI 反指標分析、Telegram 即時推送。
|
|
8
|
+
|
|
9
|
+
- 辨識她提到的標的(個股、ETF、原物料)
|
|
10
|
+
- 判斷她的操作(買入 / 被套 / 停損)
|
|
11
|
+
- 反轉推導(她停損 → 可能反彈、她買入 → 可能下跌)
|
|
12
|
+
- 推導連鎖效應(油價跌 → 製造業利多 → 電子股受惠)
|
|
13
|
+
|
|
14
|
+
> **Claude Code 使用者?** 直接把 [`skill/SKILL.md`](skill/SKILL.md) 加到你的 `.claude/skills/` 就能用。Claude 自己當分析引擎,不需要額外 LLM。
|
|
15
|
+
|
|
16
|
+
支援兩種使用模式:
|
|
17
|
+
- **常駐排程**:Docker 部署,自動盤中/盤後排程 + LLM 分析 + Telegram 推送
|
|
18
|
+
- **CLI 工具**:`npx @cablate/banini-tracker`,搭配 Claude Code 等 AI 手動執行分析
|
|
19
|
+
|
|
20
|
+
## 快速開始(常駐排程)
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# 1. 複製設定
|
|
24
|
+
cp .env.example .env
|
|
25
|
+
# 填入 APIFY_TOKEN, LLM_BASE_URL, LLM_API_KEY, LLM_MODEL, TG_BOT_TOKEN, TG_CHANNEL_ID
|
|
26
|
+
|
|
27
|
+
# 2. Docker 部署
|
|
28
|
+
docker build -t banini-tracker .
|
|
29
|
+
docker run -d --name banini --env-file .env banini-tracker
|
|
30
|
+
|
|
31
|
+
# 3. 或本地直接跑
|
|
32
|
+
npm install && npm run start
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 排程規則
|
|
36
|
+
|
|
37
|
+
- **盤中**(週一~五 09:00-13:30):每 30 分鐘,FB only 抓 1 篇
|
|
38
|
+
- **盤後**(每天 23:00):Threads + FB 各 3 篇
|
|
39
|
+
|
|
40
|
+
### npm scripts
|
|
41
|
+
|
|
42
|
+
| 指令 | 說明 |
|
|
43
|
+
|------|------|
|
|
44
|
+
| `npm run start` | 常駐排程模式(盤中 + 盤後自動跑) |
|
|
45
|
+
| `npm run dev` | 單次執行(Threads + FB 各 3 篇) |
|
|
46
|
+
| `npm run dry` | 只抓取,不呼叫 LLM |
|
|
47
|
+
| `npm run market` | 盤中模式(FB only, 1 篇) |
|
|
48
|
+
| `npm run evening` | 盤後模式(各 3 篇) |
|
|
49
|
+
|
|
50
|
+
### .env 設定
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
APIFY_TOKEN=apify_api_...
|
|
54
|
+
LLM_BASE_URL=https://api.deepinfra.com/v1/openai
|
|
55
|
+
LLM_API_KEY=...
|
|
56
|
+
LLM_MODEL=MiniMaxAI/MiniMax-M2.5
|
|
57
|
+
TG_BOT_TOKEN=...
|
|
58
|
+
TG_CHANNEL_ID=-100...
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## CLI 工具模式
|
|
62
|
+
|
|
63
|
+
不需 clone repo,任何環境直接用:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# 初始化設定
|
|
67
|
+
npx @cablate/banini-tracker init \
|
|
68
|
+
--apify-token YOUR_APIFY_TOKEN \
|
|
69
|
+
--tg-bot-token YOUR_TG_BOT_TOKEN \
|
|
70
|
+
--tg-channel-id YOUR_TG_CHANNEL_ID
|
|
71
|
+
|
|
72
|
+
# 抓取 Facebook 最新 3 篇
|
|
73
|
+
npx @cablate/banini-tracker fetch -s fb -n 3 --mark-seen
|
|
74
|
+
|
|
75
|
+
# 推送結果到 Telegram
|
|
76
|
+
npx @cablate/banini-tracker push -m "分析結果..."
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### CLI 指令
|
|
80
|
+
|
|
81
|
+
| 指令 | 說明 |
|
|
82
|
+
|------|------|
|
|
83
|
+
| `init` | 初始化設定檔(`~/.banini-tracker.json`) |
|
|
84
|
+
| `config` | 顯示目前設定 |
|
|
85
|
+
| `fetch` | 抓取貼文,輸出 JSON 到 stdout |
|
|
86
|
+
| `push` | 推送訊息到 Telegram |
|
|
87
|
+
| `seen list` | 列出已讀貼文 ID |
|
|
88
|
+
| `seen mark <id...>` | 標記貼文為已讀 |
|
|
89
|
+
| `seen clear` | 清空已讀紀錄 |
|
|
90
|
+
|
|
91
|
+
### fetch 選項
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
-s, --source <source> 來源:threads / fb / both(預設 fb)
|
|
95
|
+
-n, --limit <n> 每個來源抓幾篇(預設 3)
|
|
96
|
+
--no-dedup 不去重
|
|
97
|
+
--mark-seen 輸出後自動標記已讀
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### push 選項
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
-m, --message <text> 直接帶訊息
|
|
104
|
+
-f, --file <path> 從檔案讀取
|
|
105
|
+
--parse-mode <mode> HTML / Markdown / none(預設 HTML)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
不帶 `-m` 或 `-f` 時從 stdin 讀取。
|
|
109
|
+
|
|
110
|
+
### 搭配 Claude Code 使用
|
|
111
|
+
|
|
112
|
+
在 Claude Code 的 skill 中,Claude 自己就是分析引擎:
|
|
113
|
+
|
|
114
|
+
1. `fetch` 抓貼文 → Claude 讀 JSON
|
|
115
|
+
2. Claude 分析 + WebSearch 查最新走勢
|
|
116
|
+
3. Claude 組報告 → `push` 推送 Telegram
|
|
117
|
+
|
|
118
|
+
詳見 [`skill/SKILL.md`](skill/SKILL.md)。
|
|
119
|
+
|
|
120
|
+
## 費用
|
|
121
|
+
|
|
122
|
+
| 來源 | 每次費用 | 說明 |
|
|
123
|
+
|------|---------|------|
|
|
124
|
+
| Facebook | ~$0.02 | CU 計費,便宜 |
|
|
125
|
+
| Threads | ~$0.15 | Pay-per-event,較貴 |
|
|
126
|
+
|
|
127
|
+
## 免責聲明
|
|
128
|
+
|
|
129
|
+
本專案僅供娛樂參考,不構成任何投資建議。
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT
|
package/dist/analyze.js
CHANGED
|
@@ -1,74 +1,74 @@
|
|
|
1
1
|
import OpenAI from 'openai';
|
|
2
|
-
const SYSTEM_PROMPT = `你是一位台股投資分析助手,專門解讀「反指標女神」巴逆逆(8zz)的社群貼文。
|
|
3
|
-
|
|
4
|
-
## 背景
|
|
5
|
-
巴逆逆是台灣知名的「股海冥燈」,她的投資判斷長期被網友驗證與市場走勢高度反向。
|
|
6
|
-
買什麼跌什麼,賣什麼漲什麼,空什麼就飆漲。
|
|
7
|
-
|
|
8
|
-
## 時序意識(重要)
|
|
9
|
-
- 使用者會告訴你「現在時間」,請以此為基準判斷時效性
|
|
10
|
-
- 每篇貼文都有具體發文時間,請注意先後順序——她的想法可能在幾小時內改變
|
|
11
|
-
- **當天的貼文最重要**,前幾天的貼文參考價值遞減
|
|
12
|
-
- 如果她先說要買 A,後來又說停損 A,以最新的為準
|
|
13
|
-
|
|
14
|
-
## 反指標核心邏輯(務必遵守)
|
|
15
|
-
|
|
16
|
-
關鍵區分:她的操作狀態不同,反指標方向也不同。
|
|
17
|
-
|
|
18
|
-
| 她的狀態 | 反指標解讀 | 原因 |
|
|
19
|
-
|---------|-----------|------|
|
|
20
|
-
| **買入/加碼** | 該標的可能下跌 | 她買什麼跌什麼 |
|
|
21
|
-
| **持有中/被套(還沒賣)** | 該標的可能繼續跌 | 她還沒認輸,底部還沒到 |
|
|
22
|
-
| **停損/賣出** | 該標的可能反彈上漲 | 她認輸出場 = 底部訊號 |
|
|
23
|
-
| **看多/喊買** | 該標的可能下跌 | 她看好的通常會跌 |
|
|
24
|
-
| **看空/喊賣** | 該標的可能上漲 | 她看衰的通常會漲 |
|
|
25
|
-
| **空單/買 put** | 該標的可能飆漲 | 她空什麼就漲什麼 |
|
|
26
|
-
|
|
27
|
-
**特別注意「被套」vs「停損」**:
|
|
28
|
-
- 被套 = 她還抱著,還在賠錢 → 反指標:可能繼續跌(她還沒認輸)
|
|
29
|
-
- 停損 = 她認賠賣出了 → 反指標:可能反彈(她一賣就漲)
|
|
30
|
-
- 這兩個方向完全相反,不要搞混!
|
|
31
|
-
|
|
32
|
-
**嚴禁腦補操作**:
|
|
33
|
-
- 只根據貼文明確提到的操作判斷,不要自行推測
|
|
34
|
-
- 「停損」= 她之前買了(做多),現在賣掉認賠。不是「放空」!
|
|
35
|
-
- 「停損後漲停」= 她賣了之後股票漲了(反指標應驗),不是「放空被軋」
|
|
36
|
-
- herAction 欄位只能填貼文中明確提到的操作,不能推測或腦補
|
|
37
|
-
|
|
38
|
-
## 分析流程
|
|
39
|
-
1. **辨識標的**:她提到了哪些股票、產業、原物料、ETF?
|
|
40
|
-
- name 欄位必須用正式名稱(如「信驊」「鈦昇」「旺宏」),不要用她的暱稱(如「王」「渣男」)
|
|
41
|
-
- 她說的「王」「大佬」「股王」可能是指台積電或信驊,請根據上下文判斷是哪檔股票,用正式名稱填入
|
|
42
|
-
2. **判斷她的操作狀態**:對照上方表格,確認她是買入、被套(還在持有)、還是停損賣出?
|
|
43
|
-
3. **反轉推導**:根據上方表格反轉。說清楚「對什麼看多/看空」以及「為什麼」
|
|
44
|
-
4. **連鎖效應**:反轉後會影響哪些相關板塊?要具體講出影響鏈
|
|
45
|
-
- 例:她停損鈦昇 → 鈦昇可能反彈 → IC 設計族群連動上漲
|
|
46
|
-
- 例:她買油正二被套還沒賣 → 油價可能繼續跌 → 原物料成本降 → 製造業利多 → 電子代工股受惠
|
|
47
|
-
- 例:她停損賣出油正二 → 油價可能反彈 → 原物料成本升 → 通膨壓力回來
|
|
48
|
-
5. **信心評估**:她語氣越篤定、越興奮,反指標信號越強;越崩潰、越後悔,通常代表趨勢即將反轉
|
|
49
|
-
|
|
50
|
-
## 輸出格式(JSON)
|
|
51
|
-
所有欄位必須用繁體中文,不要用英文術語。
|
|
52
|
-
|
|
53
|
-
{
|
|
54
|
-
"hasInvestmentContent": true/false,
|
|
55
|
-
"mentionedTargets": [
|
|
56
|
-
{
|
|
57
|
-
"name": "標的名稱(如:旺宏、鈦昇、原油正二)",
|
|
58
|
-
"type": "個股 | 產業 | 原物料 | ETF | 指數",
|
|
59
|
-
"herAction": "她的操作(如:買入、停損賣出、被套、加碼、看多、看空)",
|
|
60
|
-
"reverseView": "反指標觀點(如:可能上漲、可能下跌、可能反彈、可能續跌)",
|
|
61
|
-
"confidence": "高 | 中 | 低",
|
|
62
|
-
"reasoning": "一句話解釋為什麼(如:她停損賣出通常是底部訊號)"
|
|
63
|
-
}
|
|
64
|
-
],
|
|
65
|
-
"chainAnalysis": "連鎖效應推導,講清楚 A 漲/跌 → 影響 B → 影響 C(2-3句)",
|
|
66
|
-
"actionableSuggestion": "可操作的建議方向(1-2句,用中文講)",
|
|
67
|
-
"moodScore": 1-10,
|
|
68
|
-
"summary": "一句話摘要,適合推送通知,用直白中文"
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
如果貼文與投資完全無關(純生活、搞笑),hasInvestmentContent 設為 false,其他欄位可省略,只需 summary。
|
|
2
|
+
const SYSTEM_PROMPT = `你是一位台股投資分析助手,專門解讀「反指標女神」巴逆逆(8zz)的社群貼文。
|
|
3
|
+
|
|
4
|
+
## 背景
|
|
5
|
+
巴逆逆是台灣知名的「股海冥燈」,她的投資判斷長期被網友驗證與市場走勢高度反向。
|
|
6
|
+
買什麼跌什麼,賣什麼漲什麼,空什麼就飆漲。
|
|
7
|
+
|
|
8
|
+
## 時序意識(重要)
|
|
9
|
+
- 使用者會告訴你「現在時間」,請以此為基準判斷時效性
|
|
10
|
+
- 每篇貼文都有具體發文時間,請注意先後順序——她的想法可能在幾小時內改變
|
|
11
|
+
- **當天的貼文最重要**,前幾天的貼文參考價值遞減
|
|
12
|
+
- 如果她先說要買 A,後來又說停損 A,以最新的為準
|
|
13
|
+
|
|
14
|
+
## 反指標核心邏輯(務必遵守)
|
|
15
|
+
|
|
16
|
+
關鍵區分:她的操作狀態不同,反指標方向也不同。
|
|
17
|
+
|
|
18
|
+
| 她的狀態 | 反指標解讀 | 原因 |
|
|
19
|
+
|---------|-----------|------|
|
|
20
|
+
| **買入/加碼** | 該標的可能下跌 | 她買什麼跌什麼 |
|
|
21
|
+
| **持有中/被套(還沒賣)** | 該標的可能繼續跌 | 她還沒認輸,底部還沒到 |
|
|
22
|
+
| **停損/賣出** | 該標的可能反彈上漲 | 她認輸出場 = 底部訊號 |
|
|
23
|
+
| **看多/喊買** | 該標的可能下跌 | 她看好的通常會跌 |
|
|
24
|
+
| **看空/喊賣** | 該標的可能上漲 | 她看衰的通常會漲 |
|
|
25
|
+
| **空單/買 put** | 該標的可能飆漲 | 她空什麼就漲什麼 |
|
|
26
|
+
|
|
27
|
+
**特別注意「被套」vs「停損」**:
|
|
28
|
+
- 被套 = 她還抱著,還在賠錢 → 反指標:可能繼續跌(她還沒認輸)
|
|
29
|
+
- 停損 = 她認賠賣出了 → 反指標:可能反彈(她一賣就漲)
|
|
30
|
+
- 這兩個方向完全相反,不要搞混!
|
|
31
|
+
|
|
32
|
+
**嚴禁腦補操作**:
|
|
33
|
+
- 只根據貼文明確提到的操作判斷,不要自行推測
|
|
34
|
+
- 「停損」= 她之前買了(做多),現在賣掉認賠。不是「放空」!
|
|
35
|
+
- 「停損後漲停」= 她賣了之後股票漲了(反指標應驗),不是「放空被軋」
|
|
36
|
+
- herAction 欄位只能填貼文中明確提到的操作,不能推測或腦補
|
|
37
|
+
|
|
38
|
+
## 分析流程
|
|
39
|
+
1. **辨識標的**:她提到了哪些股票、產業、原物料、ETF?
|
|
40
|
+
- name 欄位必須用正式名稱(如「信驊」「鈦昇」「旺宏」),不要用她的暱稱(如「王」「渣男」)
|
|
41
|
+
- 她說的「王」「大佬」「股王」可能是指台積電或信驊,請根據上下文判斷是哪檔股票,用正式名稱填入
|
|
42
|
+
2. **判斷她的操作狀態**:對照上方表格,確認她是買入、被套(還在持有)、還是停損賣出?
|
|
43
|
+
3. **反轉推導**:根據上方表格反轉。說清楚「對什麼看多/看空」以及「為什麼」
|
|
44
|
+
4. **連鎖效應**:反轉後會影響哪些相關板塊?要具體講出影響鏈
|
|
45
|
+
- 例:她停損鈦昇 → 鈦昇可能反彈 → IC 設計族群連動上漲
|
|
46
|
+
- 例:她買油正二被套還沒賣 → 油價可能繼續跌 → 原物料成本降 → 製造業利多 → 電子代工股受惠
|
|
47
|
+
- 例:她停損賣出油正二 → 油價可能反彈 → 原物料成本升 → 通膨壓力回來
|
|
48
|
+
5. **信心評估**:她語氣越篤定、越興奮,反指標信號越強;越崩潰、越後悔,通常代表趨勢即將反轉
|
|
49
|
+
|
|
50
|
+
## 輸出格式(JSON)
|
|
51
|
+
所有欄位必須用繁體中文,不要用英文術語。
|
|
52
|
+
|
|
53
|
+
{
|
|
54
|
+
"hasInvestmentContent": true/false,
|
|
55
|
+
"mentionedTargets": [
|
|
56
|
+
{
|
|
57
|
+
"name": "標的名稱(如:旺宏、鈦昇、原油正二)",
|
|
58
|
+
"type": "個股 | 產業 | 原物料 | ETF | 指數",
|
|
59
|
+
"herAction": "她的操作(如:買入、停損賣出、被套、加碼、看多、看空)",
|
|
60
|
+
"reverseView": "反指標觀點(如:可能上漲、可能下跌、可能反彈、可能續跌)",
|
|
61
|
+
"confidence": "高 | 中 | 低",
|
|
62
|
+
"reasoning": "一句話解釋為什麼(如:她停損賣出通常是底部訊號)"
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
"chainAnalysis": "連鎖效應推導,講清楚 A 漲/跌 → 影響 B → 影響 C(2-3句)",
|
|
66
|
+
"actionableSuggestion": "可操作的建議方向(1-2句,用中文講)",
|
|
67
|
+
"moodScore": 1-10,
|
|
68
|
+
"summary": "一句話摘要,適合推送通知,用直白中文"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
如果貼文與投資完全無關(純生活、搞笑),hasInvestmentContent 設為 false,其他欄位可省略,只需 summary。
|
|
72
72
|
注意:僅供娛樂參考,不構成投資建議。`;
|
|
73
73
|
export async function analyzePosts(posts, llmConfig) {
|
|
74
74
|
const client = new OpenAI({
|
|
@@ -83,11 +83,11 @@ export async function analyzePosts(posts, llmConfig) {
|
|
|
83
83
|
return `### 貼文 ${i + 1} ${tag}(${p.timestamp})\n${p.text}`;
|
|
84
84
|
})
|
|
85
85
|
.join('\n\n');
|
|
86
|
-
const userPrompt = `現在時間:${nowStr}(台北時間)
|
|
87
|
-
|
|
88
|
-
以下是巴逆逆最新的社群貼文(按時間從新到舊排列),請進行反指標分析。
|
|
89
|
-
注意:標記「今天」的貼文最重要,請優先分析。
|
|
90
|
-
|
|
86
|
+
const userPrompt = `現在時間:${nowStr}(台北時間)
|
|
87
|
+
|
|
88
|
+
以下是巴逆逆最新的社群貼文(按時間從新到舊排列),請進行反指標分析。
|
|
89
|
+
注意:標記「今天」的貼文最重要,請優先分析。
|
|
90
|
+
|
|
91
91
|
${formatted}`;
|
|
92
92
|
console.log('[AI] 開始反指標分析...');
|
|
93
93
|
const res = await client.chat.completions.create({
|
package/dist/index.js
CHANGED
|
@@ -14,8 +14,9 @@ import cron from 'node-cron';
|
|
|
14
14
|
import { fetchThreadsPosts } from './threads.js';
|
|
15
15
|
import { fetchFacebookPosts } from './facebook.js';
|
|
16
16
|
import { analyzePosts } from './analyze.js';
|
|
17
|
-
import { sendTelegramMessageWithConfig, formatReport } from './telegram.js';
|
|
17
|
+
import { sendTelegramMessageWithConfig, formatReport, formatFallbackReport } from './telegram.js';
|
|
18
18
|
import { filterNewPosts as filterNew, markPostsSeen } from './seen.js';
|
|
19
|
+
import { withRetry } from './retry.js';
|
|
19
20
|
// ── Config ──────────────────────────────────────────────────
|
|
20
21
|
const THREADS_USERNAME = 'banini31';
|
|
21
22
|
const FB_PAGE_URL = 'https://www.facebook.com/DieWithoutBang/';
|
|
@@ -75,20 +76,20 @@ async function runInner(opts) {
|
|
|
75
76
|
console.log(`\n=== 巴逆逆反指標追蹤器 [${opts.label}] ${now} ===\n`);
|
|
76
77
|
const apifyToken = env('APIFY_TOKEN');
|
|
77
78
|
const allPosts = [];
|
|
78
|
-
// 1. 抓取 Threads
|
|
79
|
+
// 1. 抓取 Threads(含 retry)
|
|
79
80
|
if (!opts.fbOnly) {
|
|
80
81
|
try {
|
|
81
|
-
const threadsPosts = await fetchThreadsPosts(THREADS_USERNAME, apifyToken, opts.maxPosts);
|
|
82
|
+
const threadsPosts = await withRetry(() => fetchThreadsPosts(THREADS_USERNAME, apifyToken, opts.maxPosts), { label: 'Threads', maxRetries: 2, baseDelayMs: 5000 });
|
|
82
83
|
allPosts.push(...threadsPosts.map(fromThreads));
|
|
83
84
|
}
|
|
84
85
|
catch (err) {
|
|
85
86
|
console.error(`[Threads] 抓取失敗: ${err instanceof Error ? err.message : err}`);
|
|
86
87
|
}
|
|
87
88
|
}
|
|
88
|
-
// 2. 抓取 Facebook
|
|
89
|
+
// 2. 抓取 Facebook(含 retry)
|
|
89
90
|
if (!opts.threadsOnly) {
|
|
90
91
|
try {
|
|
91
|
-
const fbPosts = await fetchFacebookPosts(FB_PAGE_URL, apifyToken, opts.maxPosts);
|
|
92
|
+
const fbPosts = await withRetry(() => fetchFacebookPosts(FB_PAGE_URL, apifyToken, opts.maxPosts), { label: 'Facebook', maxRetries: 2, baseDelayMs: 5000 });
|
|
92
93
|
allPosts.push(...fbPosts.map(fromFacebook));
|
|
93
94
|
}
|
|
94
95
|
catch (err) {
|
|
@@ -148,11 +149,20 @@ async function runInner(opts) {
|
|
|
148
149
|
console.log('所有新貼文都是純圖片,跳過分析');
|
|
149
150
|
return;
|
|
150
151
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
152
|
+
let analysis;
|
|
153
|
+
let llmFailed = false;
|
|
154
|
+
try {
|
|
155
|
+
analysis = await withRetry(() => analyzePosts(textsForAnalysis, {
|
|
156
|
+
baseUrl: env('LLM_BASE_URL', 'https://api.deepinfra.com/v1/openai'),
|
|
157
|
+
apiKey: env('LLM_API_KEY'),
|
|
158
|
+
model: env('LLM_MODEL', 'MiniMaxAI/MiniMax-M2.5'),
|
|
159
|
+
}), { label: 'LLM', maxRetries: 3, baseDelayMs: 5000 });
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
console.error(`[LLM] 分析失敗,將推送純貼文摘要: ${err instanceof Error ? err.message : err}`);
|
|
163
|
+
llmFailed = true;
|
|
164
|
+
analysis = { hasInvestmentContent: false, summary: '(LLM 分析失敗,以下為原始貼文)' };
|
|
165
|
+
}
|
|
156
166
|
// 6. 輸出結果
|
|
157
167
|
console.log('========================================');
|
|
158
168
|
console.log(' 巴逆逆反指標分析報告');
|
|
@@ -190,13 +200,16 @@ async function runInner(opts) {
|
|
|
190
200
|
timestamp: new Date(p.timestamp).toLocaleString('zh-TW', { timeZone: 'Asia/Taipei' }),
|
|
191
201
|
isToday: isToday(p.timestamp),
|
|
192
202
|
text: p.text.slice(0, 60),
|
|
203
|
+
url: p.url,
|
|
193
204
|
}));
|
|
194
|
-
const msg =
|
|
195
|
-
|
|
205
|
+
const msg = llmFailed
|
|
206
|
+
? formatFallbackReport(postSummaries)
|
|
207
|
+
: formatReport(analysis, { threads: threadCount, fb: fbCount }, postSummaries);
|
|
208
|
+
await withRetry(() => sendTelegramMessageWithConfig({ botToken: tgToken, channelId: tgChannelId }, msg), { label: 'Telegram', maxRetries: 3, baseDelayMs: 3000 });
|
|
196
209
|
console.log('[Telegram] 通知已發送');
|
|
197
210
|
}
|
|
198
211
|
catch (err) {
|
|
199
|
-
console.error(`[Telegram]
|
|
212
|
+
console.error(`[Telegram] 發送失敗(已重試 3 次): ${err instanceof Error ? err.message : err}`);
|
|
200
213
|
}
|
|
201
214
|
}
|
|
202
215
|
else {
|
package/dist/retry.d.ts
ADDED
package/dist/retry.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export async function withRetry(fn, opts = {}) {
|
|
2
|
+
const { maxRetries = 3, baseDelayMs = 3000, label = '' } = opts;
|
|
3
|
+
let lastError;
|
|
4
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
5
|
+
try {
|
|
6
|
+
return await fn();
|
|
7
|
+
}
|
|
8
|
+
catch (err) {
|
|
9
|
+
lastError = err;
|
|
10
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
11
|
+
if (attempt < maxRetries) {
|
|
12
|
+
const delay = baseDelayMs * attempt;
|
|
13
|
+
console.error(`[${label}] 第 ${attempt} 次失敗: ${msg},${delay / 1000}s 後重試...`);
|
|
14
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
console.error(`[${label}] 第 ${attempt} 次失敗: ${msg},已達重試上限`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
throw lastError;
|
|
22
|
+
}
|
package/dist/telegram.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ interface PostSummary {
|
|
|
9
9
|
timestamp: string;
|
|
10
10
|
isToday: boolean;
|
|
11
11
|
text: string;
|
|
12
|
+
url: string;
|
|
12
13
|
}
|
|
13
14
|
export declare function formatReport(analysis: {
|
|
14
15
|
summary: string;
|
|
@@ -28,4 +29,5 @@ export declare function formatReport(analysis: {
|
|
|
28
29
|
threads: number;
|
|
29
30
|
fb: number;
|
|
30
31
|
}, posts: PostSummary[]): string;
|
|
32
|
+
export declare function formatFallbackReport(posts: PostSummary[]): string;
|
|
31
33
|
export {};
|
package/dist/telegram.js
CHANGED
|
@@ -35,7 +35,8 @@ export function formatReport(analysis, postCount, posts) {
|
|
|
35
35
|
const src = p.source === 'threads' ? 'TH' : 'FB';
|
|
36
36
|
const todayTag = p.isToday ? ' [今天]' : '';
|
|
37
37
|
const preview = escapeHtml(p.text.replace(/\n/g, ' ').slice(0, 50));
|
|
38
|
-
|
|
38
|
+
const link = p.url ? ` <a href="${p.url}">原文</a>` : '';
|
|
39
|
+
lines.push(`${src}${todayTag} ${p.timestamp}|${preview}${p.text.length > 50 ? '…' : ''}${link}`);
|
|
39
40
|
}
|
|
40
41
|
lines.push('');
|
|
41
42
|
lines.push(escapeHtml(analysis.summary));
|
|
@@ -73,3 +74,17 @@ export function formatReport(analysis, postCount, posts) {
|
|
|
73
74
|
lines.push('\n<i>僅供娛樂參考,不構成投資建議</i>');
|
|
74
75
|
return lines.join('\n');
|
|
75
76
|
}
|
|
77
|
+
export function formatFallbackReport(posts) {
|
|
78
|
+
const lines = [];
|
|
79
|
+
lines.push('<b>巴逆逆貼文速報</b>(LLM 分析失敗)');
|
|
80
|
+
lines.push('');
|
|
81
|
+
for (const p of posts) {
|
|
82
|
+
const src = p.source === 'threads' ? 'TH' : 'FB';
|
|
83
|
+
const todayTag = p.isToday ? ' [今天]' : '';
|
|
84
|
+
const preview = escapeHtml(p.text.replace(/\n/g, ' ').slice(0, 80));
|
|
85
|
+
const link = p.url ? ` <a href="${p.url}">原文</a>` : '';
|
|
86
|
+
lines.push(`${src}${todayTag} ${p.timestamp}|${preview}${p.text.length > 80 ? '…' : ''}${link}`);
|
|
87
|
+
}
|
|
88
|
+
lines.push('\n<i>LLM 服務暫時無法使用,僅列出原始貼文</i>');
|
|
89
|
+
return lines.join('\n');
|
|
90
|
+
}
|
package/package.json
CHANGED
|
@@ -1,38 +1,46 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@cablate/banini-tracker",
|
|
3
|
-
"version": "2.0.
|
|
4
|
-
"description": "巴逆逆反指標追蹤器 — 常駐排程 + CLI 雙模式",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"banini-tracker": "dist/cli.js"
|
|
8
|
-
},
|
|
9
|
-
"scripts": {
|
|
10
|
-
"dev": "tsx src/index.ts",
|
|
11
|
-
"dry": "tsx src/index.ts --dry",
|
|
12
|
-
"market": "tsx src/index.ts --fb-only --max-posts=1",
|
|
13
|
-
"evening": "tsx src/index.ts --max-posts=3",
|
|
14
|
-
"cron": "tsx src/index.ts --cron",
|
|
15
|
-
"cli": "tsx src/cli.ts",
|
|
16
|
-
"build": "tsc",
|
|
17
|
-
"start": "node dist/index.js --cron",
|
|
18
|
-
"prepublishOnly": "npm run build"
|
|
19
|
-
},
|
|
20
|
-
"keywords": [
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@cablate/banini-tracker",
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"description": "巴逆逆反指標追蹤器 — 常駐排程 + CLI 雙模式",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"banini-tracker": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "tsx src/index.ts",
|
|
11
|
+
"dry": "tsx src/index.ts --dry",
|
|
12
|
+
"market": "tsx src/index.ts --fb-only --max-posts=1",
|
|
13
|
+
"evening": "tsx src/index.ts --max-posts=3",
|
|
14
|
+
"cron": "tsx src/index.ts --cron",
|
|
15
|
+
"cli": "tsx src/cli.ts",
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"start": "node dist/index.js --cron",
|
|
18
|
+
"prepublishOnly": "npm run build"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"banini",
|
|
22
|
+
"reverse-indicator",
|
|
23
|
+
"threads",
|
|
24
|
+
"facebook",
|
|
25
|
+
"apify",
|
|
26
|
+
"telegram",
|
|
27
|
+
"claude"
|
|
28
|
+
],
|
|
29
|
+
"author": "cablate",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"commander": "^13.0.0",
|
|
33
|
+
"dotenv": "^16.4.0",
|
|
34
|
+
"node-cron": "^4.2.1",
|
|
35
|
+
"openai": "^4.0.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^20.0.0",
|
|
39
|
+
"@types/node-cron": "^3.0.11",
|
|
40
|
+
"tsx": "^4.0.0",
|
|
41
|
+
"typescript": "^5.5.0"
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"dist"
|
|
45
|
+
]
|
|
46
|
+
}
|