add job logic

This commit is contained in:
王性驊 2026-01-30 17:02:35 +08:00
parent 2655fd3eb8
commit 0db2cfaeae
24 changed files with 3078 additions and 260 deletions

View File

@ -0,0 +1,37 @@
FROM python:3.11-slim
# 安裝系統依賴 (TA-Lib 與中文字體)
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
wget \
fonts-noto-cjk \
&& rm -rf /var/lib/apt/lists/*
# 安裝 TA-Lib C library
RUN wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz && \
tar -xzf ta-lib-0.4.0-src.tar.gz && \
cd ta-lib && \
./configure --prefix=/usr && \
make && \
make install && \
cd .. && \
rm -rf ta-lib ta-lib-0.4.0-src.tar.gz
# 設置工作目錄
WORKDIR /app
# 複製依賴文件
COPY requirements.txt .
# 安裝 Python 依賴
RUN pip install --no-cache-dir -r requirements.txt
# 複製腳本
COPY scripts/ ./scripts/
# 設置環境變數
ENV PYTHONUNBUFFERED=1
ENV MPLCONFIGDIR=/tmp/matplotlib
# 預設入口點
ENTRYPOINT ["python", "scripts/sakata_analyzer.py"]

View File

@ -0,0 +1,174 @@
---
description: 酒田戰法 K 線型態分析,識別 80 種反轉與延續信號
---
# 酒田戰法 (Sakata Strategy)
專業 K 線型態分析 Skill結合自動化型態偵測與 **AI 智能分析**
## 使用方式
用戶請求:`/sakata [TICKER]` 或 `分析 [TICKER] 的 K 線型態`
---
## ⚙️ Step 1: 執行型態偵測腳本
```bash
cd .agent/skills/sakata/scripts
docker run -v $(pwd)/output:/app/output sakata-skill python sakata_analyzer.py --ticker [TICKER] --days 120
```
> 首次使用需建置 Docker`docker build -t sakata-skill .`
腳本輸出:
- `output/[TICKER]_sakata.png` - K 線圖表
- `output/[TICKER]_sakata.md` - 原始偵測報告
---
## 🧠 Step 2: AI 智能分析 (核心)
讀取腳本輸出後Agent 必須進行以下 **深度分析**
### 2.1 型態品質評估
針對偵測到的每個型態,評估其可靠性:
```markdown
#### 型態品質評估
| 型態 | 日期 | 原始強度 | AI 評估 | 調整理由 |
|------|------|---------|---------|---------|
| 晨星 | 01/25 | ⭐⭐⭐ | ⭐⭐⭐⭐ | 成交量放大 2.5x 確認 |
| 吊人 | 01/28 | ⭐⭐ | ⭐ | 趨勢不明確,信號減弱 |
```
評估因素:
- **成交量確認**: 型態伴隨成交量放大更可靠
- **趨勢背景**: 反轉型態需要明確先前趨勢
- **位置**: 頂部/底部出現的反轉型態更有效
- **型態清晰度**: 標準型態 vs 變體
### 2.2 多型態衝突分析
當多個信號衝突時Agent 需判斷主導方向:
```markdown
#### 信號衝突分析
**衝突**: 01/26 空頭吞噬 vs 01/28 錘子
**AI 判斷**: 偏向看多
**理由**:
1. 錘子為更近期信號
2. 錘子出現在支撐位附近
3. RSI 已進入超賣區,增加反彈機率
```
### 2.3 趨勢整合分析
結合均線、趨勢、支撐壓力進行綜合判斷:
```markdown
#### 趨勢整合分析
**當前位置**:
- 股價 $145.20
- MA20: $142.50 (股價在上方 ✅)
- MA50: $138.80 (股價在上方 ✅)
**支撐/壓力**:
- 近期支撐: $140.00 (前低)
- 近期壓力: $150.00 (前高)
**趨勢判斷**: 短期上升趨勢,中期盤整
```
### 2.4 交易情境模擬
提供不同情境下的操作建議:
```markdown
#### 情境分析
**情境 A: 積極多頭** (信心高)
- 現價進場,停損 $138
- 目標: $155 → $165
**情境 B: 保守觀望** (信心中)
- 等待回測 $142 (MA20) 再進場
- 或等待突破 $150 確認
**情境 C: 空頭防禦** (風險意識)
- 若跌破 $140可能轉空
- 避免追高
```
---
## 📝 Step 3: 輸出綜合報告
Agent 整合所有分析,產出最終報告:
```markdown
# [TICKER] 酒田戰法智能分析報告
**分析時間**: YYYY-MM-DD HH:MM
**分析師**: AI Agent
---
## 📊 核心結論
> **主要研判: [看多/看空/中性]**
>
> 基於 [X] 個多頭信號、[Y] 個空頭信號的綜合分析,
> 目前偏向 [方向],信心度 [高/中/低]。
## 🎯 交易建議
| 項目 | 建議 | 說明 |
|------|------|------|
| 操作方向 | 買入/賣出/觀望 | ... |
| 建議進場 | $XX.XX | 理由 |
| 停損價位 | $XX.XX | 基於 ATR/支撐 |
| 第一目標 | $XX.XX | 壓力/型態目標 |
| 第二目標 | $XX.XX | 延伸目標 |
## 📈 型態分析摘要
[列出重要型態及其解讀]
## ⚠️ 風險提示
[AI 識別的主要風險因素]
## 📉 K 線圖表
![K線圖](./output/[TICKER]_sakata.png)
```
---
## 📚 型態參考文檔
當需要解釋特定型態時,參考:
- [patterns/single_candle.md](patterns/single_candle.md) - 單根型態
- [patterns/double_candle.md](patterns/double_candle.md) - 雙根型態
- [patterns/triple_candle.md](patterns/triple_candle.md) - 三根型態
- [patterns/complex_patterns.md](patterns/complex_patterns.md) - 複雜型態
---
## 🔧 技術組件
```
.agent/skills/sakata/
├── SKILL.md # 本指令檔
├── patterns/ # 型態定義文檔
├── scripts/
│ ├── Dockerfile # Docker 環境 (TA-Lib)
│ ├── sakata_analyzer.py # 型態偵測主程式
│ └── ...
└── output/ # 輸出目錄
```
## ⚠️ 重要提醒
1. 腳本輸出只是**原始數據**Agent 必須進行 Step 2 的智能分析
2. 不要只列出型態,要**解讀其意義**
3. 多個信號時要**判斷優先級**,不是簡單加總
4. 最終建議需考慮**風險報酬比**

View File

@ -0,0 +1,79 @@
# AAPL 酒田戰法分析報告 v2.0
**生成時間**: 2026-01-30 03:33
**數據範圍**: 2025-11-03 ~ 2026-01-29 (60 日)
**當前價格**: $258.28
---
## 📊 核心結論
| 指標 | 數值 |
|------|------|
| 總信號數 | 4 |
| 多頭信號 | 2 🟢 |
| 空頭信號 | 2 🔴 |
| 整體偏向 | **NEUTRAL** |
| 信心度 | ⭐⭐⭐⭐ (4.0/5) |
## 📊 趨勢背景
| 指標 | 數值 | 說明 |
|------|------|------|
| **當前位置** | 中間 | 近60日區間位置 (33%) |
| **趨勢方向** | 下降趨勢 📉 | 基於 MA20 斜率 |
| **MA20** | $258.27 | 股價在上方 ✅ |
| **MA60** | $268.87 | 股價在下方 ❌ |
| **近期高點** | $288.62 | |
| **近期低點** | $243.42 | |
---
## 🎯 最新交易建議
| 項目 | 數值 |
|------|------|
| **建議** | 🟢 買入 |
| **信號狀態** | ⚡ 特殊情況 |
| **觸發型態** | 向上窗口 |
| **信號日期** | 2026-01-27 (2 天前) |
| **建議進場** | $259.56 |
| **停損價位** | $250.22 |
| **目標價位** | $278.24 |
| **信號強度** | ⭐⭐⭐ |
> **狀態說明**: ⚠️ 二空上漲中 (強勢軋空,勿追空)
> **酒田特殊警示**: ⚠️ 二空上漲中 (強勢軋空,勿追空)
---
## 📈 偵測到的型態
| 日期 | 型態 | 英文名稱 | 方向 | 強度 |
|------|------|---------|------|------|
| 01/27 | 向上窗口 | Rising Window | 🟢 多頭 | ⭐⭐⭐⭐⭐ |
| 01/26 | 向上窗口 | Rising Window | 🟢 多頭 | ⭐⭐⭐⭐⭐ |
| 01/20 | 向下窗口 | Falling Window | 🔴 空頭 | ⭐⭐⭐⭐⭐ |
| 01/08 | 向下窗口 | Falling Window | 🔴 空頭 | ⭐⭐⭐⭐⭐ |
---
## 📉 K 線圖表
![AAPL 酒田戰法分析](AAPL_sakata.png)
---
## ⚠️ 風險提示
1. 本分析僅供參考,不構成投資建議
2. K 線型態需配合成交量、趨勢等其他指標確認
3. **三山/三川型態需跌破/突破頸線才算確立**
4. 請嚴格執行停損紀律
5. 過去表現不代表未來結果
---
*由酒田戰法 Agent Skill v2.0 自動生成*

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

View File

@ -0,0 +1,135 @@
# 複雜型態 (Complex Patterns)
酒田五法及其他複雜 K 線型態。
---
## 酒田五法 (Sakata Goho)
### 1. 三山 (Sanzan / Three Mountains)
**別名**: 三重頂、頭肩頂
**信號**: 強空頭反轉 ⭐⭐⭐⭐⭐
**描述**:
- 價格三次觸及相似高點後回落
- 形成 M 型或頭肩型結構
- 頸線跌破確認
**交易策略**:
- 進場: 跌破頸線時放空
- 停損: 最後一個高點上方
- 目標: 頸線向下投射等距離
---
### 2. 三川 (Sansen / Three Rivers)
**別名**: 三重底、頭肩底
**信號**: 強多頭反轉 ⭐⭐⭐⭐⭐
**描述**:
- 價格三次觸及相似低點後反彈
- 形成 W 型或倒頭肩型結構
- 頸線突破確認
**交易策略**:
- 進場: 突破頸線時做多
- 停損: 最後一個低點下方
- 目標: 頸線向上投射等距離
---
### 3. 三空 (Sanku / Three Gaps)
#### 三空上漲 (Three Gaps Up)
**信號**: 空頭反轉 ⭐⭐⭐⭐
**描述**: 連續三個向上跳空
**意義**: 多頭力竭,即將回調
#### 三空下跌 (Three Gaps Down)
**信號**: 多頭反轉 ⭐⭐⭐⭐
**描述**: 連續三個向下跳空
**意義**: 空頭力竭,即將反彈
**格言**: "三空不補,後市必補"
---
### 4. 三兵 (Sanpei / Three Soldiers)
#### 三白兵 (Red Three Soldiers)
見三根型態文檔
#### 三黑鴉 (Three Black Crows)
見三根型態文檔
---
### 5. 三法 (Sanpoh / Three Methods)
#### 上升三法 (Rising Three Methods)
**信號**: 多頭延續 ⭐⭐⭐⭐
**描述**:
1. 大陽線
2. 2-5 根小陰線 (在第一根範圍內)
3. 大陽線突破新高
**意義**: 健康的休整後繼續上漲
#### 下降三法 (Falling Three Methods)
**信號**: 空頭延續 ⭐⭐⭐⭐
**描述**:
1. 大陰線
2. 2-5 根小陽線 (在第一根範圍內)
3. 大陰線跌破新低
**意義**: 健康的反彈後繼續下跌
---
## 島型型態 (Island Patterns)
### 島型頂 (Island Top)
**信號**: 強空頭反轉 ⭐⭐⭐⭐⭐
**描述**:
- 向上跳空
- 數日在高位整理
- 向下跳空,形成完全隔離的「島」
### 島型底 (Island Bottom)
**信號**: 強多頭反轉 ⭐⭐⭐⭐⭐
**描述**: 島型頂的相反版本
---
## 圓弧型態
### 圓頂 (Rounding Top)
**信號**: 空頭反轉 ⭐⭐⭐
**描述**: 價格緩慢形成圓弧頂部
**週期**: 通常需要較長時間形成
### 圓底 (Rounding Bottom)
**信號**: 多頭反轉 ⭐⭐⭐
**描述**: 價格緩慢形成圓弧底部
**別名**: 碟形底
---
## 缺口型態
### 竭盡缺口 (上) (Exhaustion Gap Up)
**信號**: 空頭反轉 ⭐⭐⭐⭐
**描述**: 趨勢末端的最後一個跳空
**特徵**: 通常伴隨極大成交量
### 竭盡缺口 (下) (Exhaustion Gap Down)
**信號**: 多頭反轉 ⭐⭐⭐⭐
**描述**: 趨勢末端的最後一個跳空
---
## 其他複雜型態
- 梯底 (Ladder Bottom)
- 藏嬰吞噬 (Concealing Baby Swallow)
- 棍子三明治 (Stick Sandwich)
- 脫離 (Breakaway)
- 鋪墊 (Mat Hold)

View File

@ -0,0 +1,98 @@
# 雙根 K 線型態 (Double Candle Patterns)
25 種雙根 K 線型態定義。
---
## 反轉型態
### 1. 多頭吞噬 (Bullish Engulfing)
**信號**: 強多頭反轉
**描述**: 大陽線完全包覆前一根陰線
**條件**: 出現在下跌趨勢末端
### 2. 空頭吞噬 (Bearish Engulfing)
**信號**: 強空頭反轉
**描述**: 大陰線完全包覆前一根陽線
**條件**: 出現在上升趨勢末端
### 3. 烏雲蓋頂 (Dark Cloud Cover)
**信號**: 空頭反轉
**描述**: 陰線開盤高於前日高點,收盤低於前日實體中點
**強度**: 中等
### 4. 刺透 (Piercing)
**信號**: 多頭反轉
**描述**: 陽線開盤低於前日低點,收盤高於前日實體中點
**強度**: 中等
### 5. 孕線 (Harami)
**信號**: 反轉 (需確認)
**描述**: 小實體完全在前一根大實體內
**強度**: 較弱需等確認K線
### 6. 十字孕線 (Harami Cross)
**信號**: 反轉
**描述**: 孕線但第二根為十字星
**強度**: 比普通孕線更強
### 7. 鑷子頂 (Tweezers Top)
**信號**: 空頭反轉
**描述**: 連續兩根K線具有相同高點
**條件**: 出現在上升趨勢
### 8. 鑷子底 (Tweezers Bottom)
**信號**: 多頭反轉
**描述**: 連續兩根K線具有相同低點
**條件**: 出現在下跌趨勢
### 9. 反沖 (Kicking)
**信號**: 強反轉
**描述**: 光頭陽線後跳空光頭陰線 (或相反)
**強度**: 極強
### 10. 反擊線 (Counterattack)
**信號**: 反轉
**描述**: 兩根相反顏色K線收盤價相同
**強度**: 中等
---
## 延續型態
### 11. 頸內線 (In Neck)
**信號**: 空頭延續
**描述**: 下跌後陽線收盤僅達前日收盤
**意義**: 反彈失敗
### 12. 頸上線 (On Neck)
**信號**: 空頭延續
**描述**: 下跌後陽線收盤達前日低點
**意義**: 反彈力道不足
### 13. 切入線 (Thrusting)
**信號**: 空頭延續
**描述**: 陽線收盤達前日實體中點以下
**意義**: 反彈但未能突破
### 14. 分離線 (Separating Lines)
**信號**: 趨勢延續
**描述**: 兩根同向K線開盤價相同但跳空
**強度**: 中等
### 15. 向上窗口 (Rising Window)
**信號**: 多頭延續
**描述**: 跳空向上,留下缺口
**意義**: 強勁買盤
### 16. 向下窗口 (Falling Window)
**信號**: 空頭延續
**描述**: 跳空向下,留下缺口
**意義**: 強勁賣盤
---
## 其他雙根型態
### 17-25. 其他變體
相同低價、家鴿、陷阱、改良陷阱、二隻烏鴉等。

View File

@ -0,0 +1,108 @@
# 單根 K 線型態 (Single Candle Patterns)
15 種單根 K 線型態定義。
---
## 1. 十字星 (Doji)
**信號**: 中性/反轉
**描述**: 開盤價 ≈ 收盤價,上下影線相當
**意義**: 市場猶豫,趨勢可能反轉
---
## 2. 蜻蜓十字 (Dragonfly Doji)
**信號**: 多頭反轉
**描述**: 長下影線,無上影線,開=收
**意義**: 下跌後買盤進場,看漲信號
---
## 3. 墓碑十字 (Gravestone Doji)
**信號**: 空頭反轉
**描述**: 長上影線,無下影線,開=收
**意義**: 上漲後賣壓湧現,看跌信號
---
## 4. 長腳十字 (Long Legged Doji)
**信號**: 中性
**描述**: 極長的上下影線,開=收
**意義**: 極度猶豫,大幅波動
---
## 5. 錘子 (Hammer)
**信號**: 多頭反轉
**描述**: 小實體在上,長下影線 (≥2倍實體)
**意義**: 跌勢後的買盤反擊
---
## 6. 吊人 (Hanging Man)
**信號**: 空頭反轉
**描述**: 與錘子形態相同,但出現在上升趨勢頂部
**意義**: 賣壓開始顯現
---
## 7. 流星 (Shooting Star)
**信號**: 空頭反轉
**描述**: 小實體在下,長上影線 (≥2倍實體)
**意義**: 多頭力竭,看跌
---
## 8. 倒錘子 (Inverted Hammer)
**信號**: 多頭反轉
**描述**: 與流星相同,但出現在下跌趨勢底部
**意義**: 潛在反轉信號
---
## 9. 光頭光腳 (Marubozu)
**信號**: 趨勢延續
**描述**: 無上下影線的大陽/大陰線
**意義**: 強勁的單方向動能
---
## 10. 陀螺 (Spinning Top)
**信號**: 中性
**描述**: 小實體,短上下影線
**意義**: 市場平衡,猶豫
---
## 11. 高浪線 (High Wave)
**信號**: 中性/反轉
**描述**: 極小實體,極長上下影線
**意義**: 極度不確定性
---
## 12. 捉腰帶線 (Belt Hold)
**信號**: 反轉
**描述**: 大陽線開盤為最低價 / 大陰線開盤為最高價
**意義**: 強勁的趨勢起始
---
## 13. 探底 (Takuri)
**信號**: 多頭反轉
**描述**: 類似錘子但下影線更長 (≥3倍實體)
**意義**: 極強的買盤反擊
---
## 14. 強勢陽線 (Strong Bullish)
**信號**: 多頭延續
**描述**: 大陽線,實體佔總長度 >70%
**意義**: 強勁買盤
---
## 15. 強勢陰線 (Strong Bearish)
**信號**: 空頭延續
**描述**: 大陰線,實體佔總長度 >70%
**意義**: 強勁賣盤

View File

@ -0,0 +1,111 @@
# 三根 K 線型態 (Triple Candle Patterns)
20 種三根 K 線型態定義。
---
## 強反轉型態
### 1. 晨星 (Morning Star)
**信號**: 強多頭反轉 ⭐⭐⭐⭐⭐
**描述**:
1. 大陰線
2. 跳空小實體 (任何顏色)
3. 大陽線收盤超過第一根實體中點
**條件**: 出現在下跌趨勢末端
### 2. 十字晨星 (Morning Doji Star)
**信號**: 極強多頭反轉 ⭐⭐⭐⭐⭐
**描述**: 晨星變體,中間為十字星
**強度**: 比普通晨星更強
### 3. 夜星 (Evening Star)
**信號**: 強空頭反轉 ⭐⭐⭐⭐⭐
**描述**:
1. 大陽線
2. 跳空小實體
3. 大陰線收盤低於第一根實體中點
**條件**: 出現在上升趨勢末端
### 4. 十字夜星 (Evening Doji Star)
**信號**: 極強空頭反轉 ⭐⭐⭐⭐⭐
**描述**: 夜星變體,中間為十字星
**強度**: 比普通夜星更強
### 5. 棄嬰 (Abandoned Baby)
**信號**: 極強反轉 ⭐⭐⭐⭐⭐
**描述**: 類似晨星/夜星,但中間十字星與兩側完全跳空
**稀有度**: 非常罕見
---
## 三兵型態 (Three Soldiers)
### 6. 三白兵 (Three White Soldiers)
**信號**: 強多頭 ⭐⭐⭐⭐
**描述**: 連續三根陽線,每根開盤在前日實體內,收盤創新高
**條件**: 底部出現效果最佳
### 7. 三黑鴉 (Three Black Crows)
**信號**: 強空頭 ⭐⭐⭐⭐
**描述**: 連續三根陰線,每根開盤在前日實體內,收盤創新低
**條件**: 頂部出現效果最佳
### 8. 前進受阻 (Advance Block)
**信號**: 弱化多頭 ⭐⭐⭐
**描述**: 三白兵變體,後面陽線實體漸小,上影線漸長
**意義**: 買盤力道減弱
### 9. 相同三鴉 (Identical Three Crows)
**信號**: 極強空頭 ⭐⭐⭐⭐⭐
**描述**: 三黑鴉變體,每根開盤等於前日收盤
**強度**: 比普通三黑鴉更強
---
## 內部/外部型態
### 10. 三內部上升 (Three Inside Up)
**信號**: 多頭反轉 ⭐⭐⭐⭐
**描述**: 孕線後第三根陽線突破第一根高點
**強度**: 中高
### 11. 三內部下降 (Three Inside Down)
**信號**: 空頭反轉 ⭐⭐⭐⭐
**描述**: 孕線後第三根陰線跌破第一根低點
**強度**: 中高
### 12. 三外部上升 (Three Outside Up)
**信號**: 多頭反轉 ⭐⭐⭐⭐
**描述**: 多頭吞噬後第三根陽線確認
**強度**: 中高
### 13. 三外部下降 (Three Outside Down)
**信號**: 空頭反轉 ⭐⭐⭐⭐
**描述**: 空頭吞噬後第三根陰線確認
**強度**: 中高
---
## 其他三根型態
### 14. 南方三星 (Three Stars In South)
**信號**: 多頭反轉
**描述**: 三根依次縮短的陰線,代表賣壓衰竭
### 15. 三星 (Tristar)
**信號**: 反轉
**描述**: 連續三根十字星
### 16. 向上跳空二鴉 (Upside Gap Two Crows)
**信號**: 空頭反轉
**描述**: 跳空陰線後再一根更大陰線
### 17. 奇特三川底 (Unique 3 River)
**信號**: 多頭反轉
**描述**: 特殊底部反轉型態
### 18-20. 跳空相關型態
跳空並列三法、跳空缺口等。

View File

@ -0,0 +1,6 @@
pandas>=2.0.0
numpy>=1.24.0
yfinance>=0.2.30
matplotlib>=3.7.0
ta-lib>=0.4.28
mplfinance>=0.12.10b0

View File

@ -0,0 +1,326 @@
"""
Sakata Chart Plotter v2.0
K 線圖表繪製 + 型態標註
改良: 使用英文標籤避免中文字體問題
"""
import pandas as pd
import numpy as np
import mplfinance as mpf
import matplotlib.pyplot as plt
from matplotlib.patches import FancyBboxPatch
import matplotlib.font_manager as fm
import os
from datetime import datetime
def calculate_support_resistance(df: pd.DataFrame, window: int = 20) -> dict:
"""Calculate support and resistance levels."""
highs = df['High'].values
lows = df['Low'].values
closes = df['Close'].values
resistance_levels = []
support_levels = []
for i in range(window, len(df) - window):
if highs[i] == max(highs[i-window:i+window+1]):
resistance_levels.append(highs[i])
if lows[i] == min(lows[i-window:i+window+1]):
support_levels.append(lows[i])
def cluster_levels(levels, threshold=0.02):
if not levels:
return []
levels = sorted(levels)
clusters = [[levels[0]]]
for level in levels[1:]:
if (level - clusters[-1][-1]) / clusters[-1][-1] < threshold:
clusters[-1].append(level)
else:
clusters.append([level])
return [np.mean(c) for c in clusters]
resistance = cluster_levels(resistance_levels)[-3:] if resistance_levels else []
support = cluster_levels(support_levels)[:3] if support_levels else []
return {
'resistance': resistance,
'support': support,
'current_price': closes[-1]
}
def calculate_rsi(df: pd.DataFrame, period: int = 14) -> pd.Series:
"""Calculate RSI indicator."""
delta = df['Close'].diff()
gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean()
rs = gain / loss
rsi = 100 - (100 / (1 + rs))
return rsi
def create_sakata_chart(
df: pd.DataFrame,
detected_patterns: list,
signals: list,
ticker: str,
output_dir: str = './output'
) -> str:
"""
Create a candlestick chart with Sakata pattern annotations.
Uses English labels for better font compatibility.
"""
# Ensure output directory exists
os.makedirs(output_dir, exist_ok=True)
# Calculate technical indicators
df = df.copy()
df['MA20'] = df['Close'].rolling(window=20).mean()
df['MA50'] = df['Close'].rolling(window=50).mean()
df['RSI'] = calculate_rsi(df)
# Calculate support/resistance
sr_levels = calculate_support_resistance(df)
# Prepare annotation markers
buy_signals = []
sell_signals = []
# 去重: 每天每型態只標記一次
seen_dates_buy = set()
seen_dates_sell = set()
for pattern in detected_patterns[-30:]:
date = pattern['date']
if date in df.index:
date_key = str(date)[:10]
if pattern['direction'] == 'bullish':
if date_key not in seen_dates_buy:
buy_signals.append({
'date': date,
'price': df.loc[date, 'Low'] * 0.98,
'name': pattern['english'] # 使用英文名
})
seen_dates_buy.add(date_key)
else:
if date_key not in seen_dates_sell:
sell_signals.append({
'date': date,
'price': df.loc[date, 'High'] * 1.02,
'name': pattern['english'] # 使用英文名
})
seen_dates_sell.add(date_key)
# Create marker arrays
buy_markers = np.nan * np.ones(len(df))
sell_markers = np.nan * np.ones(len(df))
for sig in buy_signals:
if sig['date'] in df.index:
idx = df.index.get_loc(sig['date'])
buy_markers[idx] = df.iloc[idx]['Low'] * 0.97
for sig in sell_signals:
if sig['date'] in df.index:
idx = df.index.get_loc(sig['date'])
sell_markers[idx] = df.iloc[idx]['High'] * 1.03
# Define custom style
mc = mpf.make_marketcolors(
up='#26a69a',
down='#ef5350',
edge='inherit',
wick='inherit',
volume='in',
ohlc='i'
)
s = mpf.make_mpf_style(
marketcolors=mc,
gridstyle='-',
gridcolor='#e0e0e0',
y_on_right=True,
rc={
'font.size': 10,
'axes.labelsize': 12,
'axes.titlesize': 14
}
)
# Create additional plots
apds = []
# Buy/Sell markers
if not np.all(np.isnan(buy_markers)):
apds.append(mpf.make_addplot(
buy_markers, type='scatter', markersize=100,
marker='^', color='#26a69a', panel=0
))
if not np.all(np.isnan(sell_markers)):
apds.append(mpf.make_addplot(
sell_markers, type='scatter', markersize=100,
marker='v', color='#ef5350', panel=0
))
# Moving averages
apds.append(mpf.make_addplot(df['MA20'], color='#2196F3', width=1.5, panel=0))
apds.append(mpf.make_addplot(df['MA50'], color='#FF9800', width=1.5, panel=0))
# RSI in separate panel
apds.append(mpf.make_addplot(df['RSI'], color='#9C27B0', width=1, panel=2, ylabel='RSI'))
# RSI overbought/oversold lines
rsi_70 = np.full(len(df), 70)
rsi_30 = np.full(len(df), 30)
apds.append(mpf.make_addplot(rsi_70, color='#ef5350', width=0.5, linestyle='--', panel=2))
apds.append(mpf.make_addplot(rsi_30, color='#26a69a', width=0.5, linestyle='--', panel=2))
# Generate main chart (使用英文標題避免字體問題)
fig, axes = mpf.plot(
df,
type='candle',
style=s,
title=f'\n{ticker} - Sakata Pattern Analysis',
ylabel='Price',
ylabel_lower='Volume',
volume=True,
addplot=apds if apds else None,
figsize=(18, 12),
returnfig=True,
panel_ratios=(5, 1.5, 1.5)
)
ax = axes[0]
# Draw support lines
for level in sr_levels['support']:
ax.axhline(y=level, color='#26a69a', linestyle='--', linewidth=1.5, alpha=0.7)
ax.text(df.index[2], level, f' Support ${level:.2f}',
fontsize=9, color='#26a69a', va='bottom')
# Draw resistance lines
for level in sr_levels['resistance']:
ax.axhline(y=level, color='#ef5350', linestyle='--', linewidth=1.5, alpha=0.7)
ax.text(df.index[2], level, f' Resistance ${level:.2f}',
fontsize=9, color='#ef5350', va='top')
# Add signal summary box (使用英文)
bullish_count = len(set(p['date'] for p in detected_patterns if p['direction'] == 'bullish'))
bearish_count = len(set(p['date'] for p in detected_patterns if p['direction'] == 'bearish'))
summary_text = (
f"Signal Summary\n"
f"{'='*14}\n"
f"Bullish: {bullish_count}\n"
f"Bearish: {bearish_count}\n"
f"{'='*14}\n"
f"Close: ${sr_levels['current_price']:.2f}"
)
ax.text(
0.02, 0.98, summary_text,
transform=ax.transAxes, fontsize=10,
verticalalignment='top',
bbox=dict(boxstyle='round,pad=0.5', facecolor='white', alpha=0.9, edgecolor='#ccc'),
family='monospace'
)
# Add recent patterns annotation (使用英文)
if detected_patterns:
# 去重
seen = set()
unique_recent = []
for p in detected_patterns[:10]:
key = (str(p['date'])[:10], p['english'])
if key not in seen:
seen.add(key)
unique_recent.append(p)
pattern_text = "Recent Patterns\n" + "="*16 + "\n"
for p in unique_recent[:6]:
date_str = p['date'].strftime('%m/%d') if hasattr(p['date'], 'strftime') else str(p['date'])[:10]
signal_emoji = '+' if p['direction'] == 'bullish' else '-'
# 截斷過長的型態名稱
name = p['english'][:18] if len(p['english']) > 18 else p['english']
pattern_text += f"{date_str}: {name} [{signal_emoji}]\n"
ax.text(
0.98, 0.98, pattern_text,
transform=ax.transAxes, fontsize=9,
verticalalignment='top', horizontalalignment='right',
bbox=dict(boxstyle='round,pad=0.5', facecolor='white', alpha=0.9, edgecolor='#ccc'),
family='monospace'
)
# Add legend
from matplotlib.lines import Line2D
legend_elements = [
Line2D([0], [0], color='#2196F3', linewidth=2, label='MA20'),
Line2D([0], [0], color='#FF9800', linewidth=2, label='MA50'),
Line2D([0], [0], color='#26a69a', linestyle='--', linewidth=1.5, label='Support'),
Line2D([0], [0], color='#ef5350', linestyle='--', linewidth=1.5, label='Resistance'),
Line2D([0], [0], marker='^', color='#26a69a', linestyle='None', markersize=10, label='Bullish Signal'),
Line2D([0], [0], marker='v', color='#ef5350', linestyle='None', markersize=10, label='Bearish Signal'),
]
ax.legend(handles=legend_elements, loc='upper center', ncol=6, fontsize=8,
bbox_to_anchor=(0.5, 1.02), framealpha=0.9)
# Save chart
output_path = os.path.join(output_dir, f'{ticker}_sakata.png')
fig.savefig(output_path, dpi=150, bbox_inches='tight', facecolor='white')
plt.close(fig)
return output_path
def create_pattern_detail_chart(
df: pd.DataFrame,
pattern: dict,
ticker: str,
output_dir: str = './output'
) -> str:
"""
Create a detailed chart for a specific pattern occurrence.
"""
# Focus on 20 days around the pattern
pattern_date = pattern['date']
if pattern_date in df.index:
idx = df.index.get_loc(pattern_date)
start_idx = max(0, idx - 10)
end_idx = min(len(df), idx + 10)
focused_df = df.iloc[start_idx:end_idx].copy()
else:
focused_df = df.tail(20).copy()
# Create marker for the pattern
marker = np.nan * np.ones(len(focused_df))
if pattern_date in focused_df.index:
local_idx = focused_df.index.get_loc(pattern_date)
marker[local_idx] = focused_df.iloc[local_idx]['Low'] * 0.97
mc = mpf.make_marketcolors(up='#26a69a', down='#ef5350', edge='inherit', wick='inherit')
s = mpf.make_mpf_style(marketcolors=mc, gridstyle='-', gridcolor='#e0e0e0')
apds = []
if not np.all(np.isnan(marker)):
apds.append(mpf.make_addplot(marker, type='scatter', markersize=200, marker='*', color='gold'))
fig, axes = mpf.plot(
focused_df,
type='candle',
style=s,
title=f"\n{ticker} - {pattern['english']}",
figsize=(10, 6),
returnfig=True,
addplot=apds if apds else None
)
output_path = os.path.join(output_dir, f"{ticker}_{pattern['pattern']}_detail.png")
fig.savefig(output_path, dpi=150, bbox_inches='tight', facecolor='white')
plt.close(fig)
return output_path

View File

@ -0,0 +1,82 @@
# AAPL 酒田戰法分析報告
**生成時間**: 2026-01-30 02:43
**數據範圍**: 2025-09-02 ~ 2026-01-29 (104 日)
**當前價格**: $258.28
---
## 📊 信號摘要
| 指標 | 數值 |
|------|------|
| 總信號數 | 57 |
| 多頭信號 | 17 🟢 |
| 空頭信號 | 40 🔴 |
| 整體偏向 | **BEARISH** |
| 平均強度 | ⭐⭐⭐⭐ (4.9/5) |
---
## 🎯 最新交易建議
| 項目 | 數值 |
|------|------|
| **建議** | 🔴 賣出 |
| **觸發型態** | 三山 |
| **信號日期** | 2026-01-29 |
| **建議進場** | $256.99 |
| **停損價位** | $266.44 |
| **目標價位** | $238.08 |
| **信號強度** | ⭐⭐⭐⭐⭐ |
---
## 📈 偵測到的型態
| 日期 | 型態 | 英文名稱 | 方向 | 強度 |
|------|------|---------|------|------|
| 01/29 | 三山 | Three Mountains (Triple Top) | 🔴 空頭 | ⭐⭐⭐⭐⭐ |
| 01/28 | 三山 | Three Mountains (Triple Top) | 🔴 空頭 | ⭐⭐⭐⭐⭐ |
| 01/27 | 三山 | Three Mountains (Triple Top) | 🔴 空頭 | ⭐⭐⭐⭐⭐ |
| 01/27 | 三川 | Three Rivers (Triple Bottom) | 🟢 多頭 | ⭐⭐⭐⭐⭐ |
| 01/27 | 向上窗口 | Rising Window | 🟢 多頭 | ⭐⭐⭐⭐⭐ |
| 01/26 | 三山 | Three Mountains (Triple Top) | 🔴 空頭 | ⭐⭐⭐⭐⭐ |
| 01/26 | 三川 | Three Rivers (Triple Bottom) | 🟢 多頭 | ⭐⭐⭐⭐⭐ |
| 01/26 | 向上窗口 | Rising Window | 🟢 多頭 | ⭐⭐⭐⭐⭐ |
| 01/23 | 三山 | Three Mountains (Triple Top) | 🔴 空頭 | ⭐⭐⭐⭐⭐ |
| 01/23 | 三川 | Three Rivers (Triple Bottom) | 🟢 多頭 | ⭐⭐⭐⭐⭐ |
| 01/22 | 三山 | Three Mountains (Triple Top) | 🔴 空頭 | ⭐⭐⭐⭐⭐ |
| 01/22 | 三川 | Three Rivers (Triple Bottom) | 🟢 多頭 | ⭐⭐⭐⭐⭐ |
| 01/21 | 三山 | Three Mountains (Triple Top) | 🔴 空頭 | ⭐⭐⭐⭐⭐ |
| 01/21 | 三川 | Three Rivers (Triple Bottom) | 🟢 多頭 | ⭐⭐⭐⭐⭐ |
| 01/20 | 三山 | Three Mountains (Triple Top) | 🔴 空頭 | ⭐⭐⭐⭐⭐ |
---
## 📉 K 線圖表
![AAPL 酒田戰法分析](AAPL_sakata.png)
---
## 📚 型態類型說明
| 類型 | 說明 |
|------|------|
| 反轉型態 | 趨勢可能即將改變,需配合其他指標確認 |
| 延續型態 | 當前趨勢可能持續,可順勢操作 |
| 中性型態 | 市場猶豫,建議觀望 |
---
## ⚠️ 風險提示
1. 本分析僅供參考,不構成投資建議
2. K 線型態需配合成交量、趨勢等其他指標確認
3. 請嚴格執行停損紀律
4. 過去表現不代表未來結果
---
*由酒田戰法 Agent Skill 自動生成*

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

@ -0,0 +1,758 @@
"""
Sakata 80 Candlestick Pattern Detector v2.0
酒田戰法 80 K 線型態偵測器 (改良版)
改良重點:
1. 趨勢濾網: 高檔偵測三山, 低檔偵測三川
2. 時間跨度: 三山/三川至少間隔 10 K
3. 結構分析: 計算頸線量價背離
"""
import numpy as np
import pandas as pd
import talib
# Pattern categories and their TA-Lib function names
TALIB_PATTERNS = {
# Single Candle Patterns (單根型態)
'CDL_DOJI': {'name': '十字星', 'en': 'Doji', 'type': 'reversal', 'signal': 'neutral'},
'CDL_DOJISTAR': {'name': '十字星', 'en': 'Doji Star', 'type': 'reversal', 'signal': 'neutral'},
'CDL_DRAGONFLYDOJI': {'name': '蜻蜓十字', 'en': 'Dragonfly Doji', 'type': 'reversal', 'signal': 'bullish'},
'CDL_GRAVESTONEDOJI': {'name': '墓碑十字', 'en': 'Gravestone Doji', 'type': 'reversal', 'signal': 'bearish'},
'CDL_LONGLEGGEDDOJI': {'name': '長腳十字', 'en': 'Long Legged Doji', 'type': 'reversal', 'signal': 'neutral'},
'CDL_HAMMER': {'name': '錘子', 'en': 'Hammer', 'type': 'reversal', 'signal': 'bullish'},
'CDL_HANGINGMAN': {'name': '吊人', 'en': 'Hanging Man', 'type': 'reversal', 'signal': 'bearish'},
'CDL_SHOOTINGSTAR': {'name': '流星', 'en': 'Shooting Star', 'type': 'reversal', 'signal': 'bearish'},
'CDL_INVERTEDHAMMER': {'name': '倒錘子', 'en': 'Inverted Hammer', 'type': 'reversal', 'signal': 'bullish'},
'CDL_MARUBOZU': {'name': '光頭光腳', 'en': 'Marubozu', 'type': 'continuation', 'signal': 'trend'},
'CDL_SPINNINGTOP': {'name': '陀螺', 'en': 'Spinning Top', 'type': 'reversal', 'signal': 'neutral'},
'CDL_HIGHWAVE': {'name': '高浪線', 'en': 'High Wave', 'type': 'reversal', 'signal': 'neutral'},
'CDL_BELTHOLD': {'name': '捉腰帶線', 'en': 'Belt Hold', 'type': 'reversal', 'signal': 'trend'},
'CDL_TAKURI': {'name': '探底', 'en': 'Takuri', 'type': 'reversal', 'signal': 'bullish'},
# Double Candle Patterns (雙根型態)
'CDL_ENGULFING': {'name': '吞噬', 'en': 'Engulfing', 'type': 'reversal', 'signal': 'trend'},
'CDL_DARKCLOUDCOVER': {'name': '烏雲蓋頂', 'en': 'Dark Cloud Cover', 'type': 'reversal', 'signal': 'bearish'},
'CDL_PIERCING': {'name': '刺透', 'en': 'Piercing', 'type': 'reversal', 'signal': 'bullish'},
'CDL_HARAMI': {'name': '孕線', 'en': 'Harami', 'type': 'reversal', 'signal': 'trend'},
'CDL_HARAMICROSS': {'name': '十字孕線', 'en': 'Harami Cross', 'type': 'reversal', 'signal': 'trend'},
'CDL_MATCHINGLOW': {'name': '相同低價', 'en': 'Matching Low', 'type': 'reversal', 'signal': 'bullish'},
'CDL_KICKING': {'name': '反沖', 'en': 'Kicking', 'type': 'reversal', 'signal': 'trend'},
'CDL_KICKINGBYLENGTH': {'name': '反沖(長度)', 'en': 'Kicking by Length', 'type': 'reversal', 'signal': 'trend'},
'CDL_COUNTERATTACK': {'name': '反擊線', 'en': 'Counterattack', 'type': 'reversal', 'signal': 'trend'},
'CDL_CLOSINGMARUBOZU': {'name': '收盤光頭', 'en': 'Closing Marubozu', 'type': 'continuation', 'signal': 'trend'},
'CDL_HIKKAKE': {'name': '陷阱', 'en': 'Hikkake', 'type': 'reversal', 'signal': 'trend'},
'CDL_HIKKAKEMOD': {'name': '改良陷阱', 'en': 'Modified Hikkake', 'type': 'reversal', 'signal': 'trend'},
'CDL_HOMINGPIGEON': {'name': '家鴿', 'en': 'Homing Pigeon', 'type': 'reversal', 'signal': 'bullish'},
'CDL_INNECK': {'name': '頸內線', 'en': 'In Neck', 'type': 'continuation', 'signal': 'bearish'},
'CDL_ONNECK': {'name': '頸上線', 'en': 'On Neck', 'type': 'continuation', 'signal': 'bearish'},
'CDL_THRUSTING': {'name': '切入線', 'en': 'Thrusting', 'type': 'continuation', 'signal': 'bearish'},
'CDL_SEPARATINGLINES': {'name': '分離線', 'en': 'Separating Lines', 'type': 'continuation', 'signal': 'trend'},
'CDL_TWOCROWS': {'name': '二隻烏鴉', 'en': 'Two Crows', 'type': 'reversal', 'signal': 'bearish'},
# Triple Candle Patterns (三根型態)
'CDL_MORNINGSTAR': {'name': '晨星', 'en': 'Morning Star', 'type': 'reversal', 'signal': 'bullish'},
'CDL_MORNINGDOJISTAR': {'name': '十字晨星', 'en': 'Morning Doji Star', 'type': 'reversal', 'signal': 'bullish'},
'CDL_EVENINGSTAR': {'name': '夜星', 'en': 'Evening Star', 'type': 'reversal', 'signal': 'bearish'},
'CDL_EVENINGDOJISTAR': {'name': '十字夜星', 'en': 'Evening Doji Star', 'type': 'reversal', 'signal': 'bearish'},
'CDL_ABANDONEDBABY': {'name': '棄嬰', 'en': 'Abandoned Baby', 'type': 'reversal', 'signal': 'trend'},
'CDL_3WHITESOLDIERS': {'name': '三白兵', 'en': 'Three White Soldiers', 'type': 'reversal', 'signal': 'bullish'},
'CDL_3BLACKCROWS': {'name': '三黑鴉', 'en': 'Three Black Crows', 'type': 'reversal', 'signal': 'bearish'},
'CDL_3INSIDE': {'name': '三內部', 'en': 'Three Inside', 'type': 'reversal', 'signal': 'trend'},
'CDL_3OUTSIDE': {'name': '三外部', 'en': 'Three Outside', 'type': 'reversal', 'signal': 'trend'},
'CDL_3STARSINSOUTH': {'name': '南方三星', 'en': 'Three Stars In South', 'type': 'reversal', 'signal': 'bullish'},
'CDL_TRISTAR': {'name': '三星', 'en': 'Tristar', 'type': 'reversal', 'signal': 'trend'},
'CDL_UPSIDEGAP2CROWS': {'name': '向上跳空二鴉', 'en': 'Upside Gap Two Crows', 'type': 'reversal', 'signal': 'bearish'},
'CDL_UNIQUE3RIVER': {'name': '奇特三川底', 'en': 'Unique 3 River', 'type': 'reversal', 'signal': 'bullish'},
'CDL_XSIDEGAP3METHODS': {'name': '跳空並列三法', 'en': 'Side Gap Three Methods', 'type': 'continuation', 'signal': 'trend'},
'CDL_TASUKIGAP': {'name': '跳空缺口', 'en': 'Tasuki Gap', 'type': 'continuation', 'signal': 'trend'},
'CDL_GAPSIDESIDEWHITE': {'name': '向上跳空並列陽線', 'en': 'Gap Side Side White', 'type': 'continuation', 'signal': 'bullish'},
# Complex Patterns (複雜型態)
'CDL_ADVANCEBLOCK': {'name': '前進受阻', 'en': 'Advance Block', 'type': 'reversal', 'signal': 'bearish'},
'CDL_STALLEDPATTERN': {'name': '停滯型態', 'en': 'Stalled Pattern', 'type': 'reversal', 'signal': 'bearish'},
'CDL_CONCEALBABYSWALL': {'name': '藏嬰吞噬', 'en': 'Concealing Baby Swallow', 'type': 'reversal', 'signal': 'bullish'},
'CDL_LADDERBOTTOM': {'name': '梯底', 'en': 'Ladder Bottom', 'type': 'reversal', 'signal': 'bullish'},
'CDL_RICKSHAWMAN': {'name': '黃包車夫', 'en': 'Rickshaw Man', 'type': 'reversal', 'signal': 'neutral'},
'CDL_RISEFALL3METHODS': {'name': '上升/下降三法', 'en': 'Rise/Fall Three Methods', 'type': 'continuation', 'signal': 'trend'},
'CDL_STICKSANDWICH': {'name': '棍子三明治', 'en': 'Stick Sandwich', 'type': 'reversal', 'signal': 'bullish'},
'CDL_BREAKAWAY': {'name': '脫離', 'en': 'Breakaway', 'type': 'reversal', 'signal': 'trend'},
'CDL_MATHOLD': {'name': '鋪墊', 'en': 'Mat Hold', 'type': 'continuation', 'signal': 'bullish'},
'CDL_IDENTICAL3CROWS': {'name': '相同三鴉', 'en': 'Identical Three Crows', 'type': 'reversal', 'signal': 'bearish'},
'CDL_SHORTLINE': {'name': '短線', 'en': 'Short Line', 'type': 'neutral', 'signal': 'neutral'},
'CDL_LONGLINE': {'name': '長線', 'en': 'Long Line', 'type': 'continuation', 'signal': 'trend'},
}
# Custom patterns not in TA-Lib (酒田特有型態)
CUSTOM_PATTERNS = {
'SANZAN': {'name': '三山', 'en': 'Three Mountains (Triple Top)', 'type': 'reversal', 'signal': 'bearish'},
'SANSEN': {'name': '三川', 'en': 'Three Rivers (Triple Bottom)', 'type': 'reversal', 'signal': 'bullish'},
'SANKU_UP': {'name': '三空(上漲)', 'en': 'Three Gaps Up', 'type': 'reversal', 'signal': 'bearish'},
'SANKU_DOWN': {'name': '三空(下跌)', 'en': 'Three Gaps Down', 'type': 'reversal', 'signal': 'bullish'},
'SANPOH_UP': {'name': '上升三法', 'en': 'Rising Three Methods', 'type': 'continuation', 'signal': 'bullish'},
'SANPOH_DOWN': {'name': '下降三法', 'en': 'Falling Three Methods', 'type': 'continuation', 'signal': 'bearish'},
'ISLAND_TOP': {'name': '島型頂', 'en': 'Island Top', 'type': 'reversal', 'signal': 'bearish'},
'ISLAND_BOTTOM': {'name': '島型底', 'en': 'Island Bottom', 'type': 'reversal', 'signal': 'bullish'},
'ROUND_TOP': {'name': '圓頂', 'en': 'Rounding Top', 'type': 'reversal', 'signal': 'bearish'},
'ROUND_BOTTOM': {'name': '圓底', 'en': 'Rounding Bottom', 'type': 'reversal', 'signal': 'bullish'},
'TWEEZERS_TOP': {'name': '鑷子頂', 'en': 'Tweezers Top', 'type': 'reversal', 'signal': 'bearish'},
'TWEEZERS_BOTTOM': {'name': '鑷子底', 'en': 'Tweezers Bottom', 'type': 'reversal', 'signal': 'bullish'},
'WINDOW_UP': {'name': '向上窗口', 'en': 'Rising Window', 'type': 'continuation', 'signal': 'bullish'},
'WINDOW_DOWN': {'name': '向下窗口', 'en': 'Falling Window', 'type': 'continuation', 'signal': 'bearish'},
'EXHAUSTION_UP': {'name': '竭盡缺口(上)', 'en': 'Exhaustion Gap Up', 'type': 'reversal', 'signal': 'bearish'},
'EXHAUSTION_DOWN': {'name': '竭盡缺口(下)', 'en': 'Exhaustion Gap Down', 'type': 'reversal', 'signal': 'bullish'},
'STRONG_BULL': {'name': '強勢陽線', 'en': 'Strong Bullish Candle', 'type': 'continuation', 'signal': 'bullish'},
'STRONG_BEAR': {'name': '強勢陰線', 'en': 'Strong Bearish Candle', 'type': 'continuation', 'signal': 'bearish'},
}
# ============================================================
# 趨勢濾網 (Trend Filter)
# ============================================================
def calculate_trend_context(df: pd.DataFrame) -> dict:
"""
計算趨勢背景資訊用於過濾不合理的信號
Returns:
dict: {
'ma20': MA20 ,
'ma60': MA60 ,
'position': 'high' | 'mid' | 'low',
'trend': 'uptrend' | 'downtrend' | 'sideways'
}
"""
close = df['Close'].values
# 計算均線
ma20 = talib.SMA(close, timeperiod=20)
ma60 = talib.SMA(close, timeperiod=60)
# 計算 ATR 用於判斷波動性
atr = talib.ATR(df['High'].values, df['Low'].values, close, timeperiod=14)
# 計算過去 60 天的高低點
lookback = min(60, len(df))
recent_high = df['High'].iloc[-lookback:].max()
recent_low = df['Low'].iloc[-lookback:].min()
price_range = recent_high - recent_low
# 當前價格在區間中的位置
current_price = close[-1]
position_pct = (current_price - recent_low) / price_range if price_range > 0 else 0.5
if position_pct > 0.7:
position = 'high'
elif position_pct < 0.3:
position = 'low'
else:
position = 'mid'
# 趨勢判斷 (基於 MA20 斜率)
if len(ma20) >= 10:
ma20_slope = (ma20[-1] - ma20[-10]) / ma20[-10] if ma20[-10] > 0 else 0
if ma20_slope > 0.02:
trend = 'uptrend'
elif ma20_slope < -0.02:
trend = 'downtrend'
else:
trend = 'sideways'
else:
trend = 'sideways'
return {
'ma20': ma20,
'ma60': ma60,
'atr': atr,
'position': position,
'position_pct': position_pct,
'trend': trend,
'recent_high': recent_high,
'recent_low': recent_low
}
# ============================================================
# 結構分析器 (Structure Analyzer)
# ============================================================
def find_swing_points(prices: np.ndarray, min_distance: int = 5) -> tuple:
"""
找出擺盪高低點確保相鄰點之間有最小間距
Args:
prices: 價格陣列
min_distance: 相鄰擺盪點的最小間距
Returns:
(peaks, troughs): 高點和低點的索引列表
"""
peaks = []
troughs = []
for i in range(min_distance, len(prices) - min_distance):
# 找高點
is_peak = True
for j in range(1, min_distance + 1):
if prices[i] <= prices[i - j] or prices[i] <= prices[i + j]:
is_peak = False
break
if is_peak:
# 確保和前一個高點有足夠間距
if not peaks or (i - peaks[-1]) >= min_distance:
peaks.append(i)
# 找低點
is_trough = True
for j in range(1, min_distance + 1):
if prices[i] >= prices[i - j] or prices[i] >= prices[i + j]:
is_trough = False
break
if is_trough:
if not troughs or (i - troughs[-1]) >= min_distance:
troughs.append(i)
return peaks, troughs
def calculate_neckline(df: pd.DataFrame, peaks: list, troughs: list, pattern_type: str) -> dict:
"""
計算頸線位置
Args:
df: DataFrame
peaks: 高點索引列表
troughs: 低點索引列表
pattern_type: 'sanzan' (三山) 'sansen' (三川)
Returns:
dict: {
'neckline': 頸線價格,
'confirmed': 是否已跌破/突破頸線,
'distance_pct': 當前價格距離頸線的百分比
}
"""
if pattern_type == 'sanzan' and len(troughs) >= 2:
# 三山的頸線 = 兩個谷底的連線 (取較高者)
trough_prices = [df['Low'].iloc[t] for t in troughs[-2:]]
neckline = max(trough_prices)
current_price = df['Close'].iloc[-1]
confirmed = current_price < neckline
distance_pct = (current_price - neckline) / neckline * 100
elif pattern_type == 'sansen' and len(peaks) >= 2:
# 三川的頸線 = 兩個高點的連線 (取較低者)
peak_prices = [df['High'].iloc[p] for p in peaks[-2:]]
neckline = min(peak_prices)
current_price = df['Close'].iloc[-1]
confirmed = current_price > neckline
distance_pct = (current_price - neckline) / neckline * 100
else:
return {'neckline': None, 'confirmed': False, 'distance_pct': 0}
return {
'neckline': neckline,
'confirmed': confirmed,
'distance_pct': distance_pct
}
def check_volume_divergence(df: pd.DataFrame, peaks: list) -> dict:
"""
檢查量價背離
三山時第三座山的成交量應該萎縮 (看空確認)
三川時第三個底的成交量應該萎縮然後放大 (看多確認)
Returns:
dict: {
'divergence': bool,
'type': 'bearish' | 'bullish' | None,
'description': 說明
}
"""
if 'Volume' not in df.columns or len(peaks) < 3:
return {'divergence': False, 'type': None, 'description': '無成交量資料'}
volumes = df['Volume'].values
# 取最後三個高點/低點的成交量
peak_volumes = [volumes[p] for p in peaks[-3:]]
# 量價背離: 價格創新高,但成交量遞減
vol_trend = (peak_volumes[-1] < peak_volumes[-2] < peak_volumes[-3])
if vol_trend:
return {
'divergence': True,
'type': 'bearish',
'description': f'量價背離: 成交量由 {peak_volumes[-3]:,.0f}{peak_volumes[-2]:,.0f}{peak_volumes[-1]:,.0f} 遞減'
}
return {'divergence': False, 'type': None, 'description': '成交量正常'}
# ============================================================
# 改良版三山/三川偵測
# ============================================================
def detect_sanzan_v2(df: pd.DataFrame, trend_ctx: dict,
lookback: int = 60, min_peak_distance: int = 10) -> dict:
"""
改良版三山 (Triple Top) 偵測
改良點:
1. 只在高檔 (position = 'high') 才偵測
2. 三個高點至少間隔 min_peak_distance K
3. 計算頸線和量價背離
Returns:
dict: 完整的型態分析結果
"""
signals = np.zeros(len(df))
analysis = {
'detected': False,
'stage': 0, # 1-5 階段
'peaks': [],
'neckline': None,
'volume_divergence': None,
'description': ''
}
# 濾網1: 只在高檔偵測三山
if trend_ctx['position'] not in ['high', 'mid']:
analysis['description'] = '股價位於低檔,不適合偵測三山'
return {'signals': signals, 'analysis': analysis}
highs = df['High'].values
# 找擺盪高點 (至少間隔 min_peak_distance)
peaks, troughs = find_swing_points(highs, min_distance=min_peak_distance)
if len(peaks) < 3:
analysis['description'] = f'高點數量不足 ({len(peaks)}/3)'
return {'signals': signals, 'analysis': analysis}
# 取最後三個高點
last_3_peaks = peaks[-3:]
peak_values = [highs[p] for p in last_3_peaks]
avg_peak = np.mean(peak_values)
# 檢查三個高點是否在相近水平 (容許 5% 誤差)
tolerance = 0.05
peaks_aligned = all(abs(p - avg_peak) / avg_peak < tolerance for p in peak_values)
if not peaks_aligned:
analysis['description'] = '三個高點水平差異過大'
return {'signals': signals, 'analysis': analysis}
# 檢查時間跨度 (三個高點的總跨度應該夠大)
total_span = last_3_peaks[-1] - last_3_peaks[0]
if total_span < 20: # 至少跨越 20 根 K 線
analysis['description'] = f'時間跨度不足 ({total_span}/20 根K線)'
return {'signals': signals, 'analysis': analysis}
# 計算頸線
neckline_info = calculate_neckline(df, last_3_peaks, troughs, 'sanzan')
# 檢查量價背離
volume_info = check_volume_divergence(df, last_3_peaks)
# 判斷型態階段
current_price = df['Close'].iloc[-1]
if neckline_info['neckline']:
if neckline_info['confirmed']:
stage = 5 # 已跌破頸線,型態確立
elif current_price < avg_peak * 0.97:
stage = 4 # 正在形成右肩
else:
stage = 3 # 第三山頂形成中
else:
stage = 2 # 僅形成兩個山頂
# 只在型態較成熟時才發出信號 (stage >= 3)
if stage >= 3:
signals[-1] = -100 if stage >= 4 else -50
analysis['detected'] = True
analysis.update({
'stage': stage,
'peaks': [{'idx': p, 'price': highs[p], 'date': df.index[p]} for p in last_3_peaks],
'neckline': neckline_info,
'volume_divergence': volume_info,
'avg_peak': avg_peak,
'description': f'三山型態 ({stage}/5 階段)' +
(' ⚠️ 量價背離' if volume_info['divergence'] else '')
})
return {'signals': signals, 'analysis': analysis}
def detect_sansen_v2(df: pd.DataFrame, trend_ctx: dict,
lookback: int = 60, min_trough_distance: int = 10) -> dict:
"""
改良版三川 (Triple Bottom) 偵測
改良點:
1. 只在低檔 (position = 'low') 才偵測
2. 三個低點至少間隔 min_trough_distance K
3. 計算頸線和量能確認
"""
signals = np.zeros(len(df))
analysis = {
'detected': False,
'stage': 0,
'troughs': [],
'neckline': None,
'volume_divergence': None,
'description': ''
}
# 濾網1: 只在低檔偵測三川
if trend_ctx['position'] not in ['low', 'mid']:
analysis['description'] = '股價位於高檔,不適合偵測三川'
return {'signals': signals, 'analysis': analysis}
lows = df['Low'].values
# 找擺盪低點
peaks, troughs = find_swing_points(lows, min_distance=min_trough_distance)
if len(troughs) < 3:
analysis['description'] = f'低點數量不足 ({len(troughs)}/3)'
return {'signals': signals, 'analysis': analysis}
# 取最後三個低點
last_3_troughs = troughs[-3:]
trough_values = [lows[t] for t in last_3_troughs]
avg_trough = np.mean(trough_values)
# 檢查三個低點是否在相近水平
tolerance = 0.05
troughs_aligned = all(abs(t - avg_trough) / avg_trough < tolerance for t in trough_values)
if not troughs_aligned:
analysis['description'] = '三個低點水平差異過大'
return {'signals': signals, 'analysis': analysis}
# 時間跨度檢查
total_span = last_3_troughs[-1] - last_3_troughs[0]
if total_span < 20:
analysis['description'] = f'時間跨度不足 ({total_span}/20 根K線)'
return {'signals': signals, 'analysis': analysis}
# 計算頸線
neckline_info = calculate_neckline(df, peaks, last_3_troughs, 'sansen')
# 判斷型態階段
current_price = df['Close'].iloc[-1]
if neckline_info['neckline']:
if neckline_info['confirmed']:
stage = 5
elif current_price > avg_trough * 1.03:
stage = 4
else:
stage = 3
else:
stage = 2
if stage >= 3:
signals[-1] = 100 if stage >= 4 else 50
analysis['detected'] = True
analysis.update({
'stage': stage,
'troughs': [{'idx': t, 'price': lows[t], 'date': df.index[t]} for t in last_3_troughs],
'neckline': neckline_info,
'avg_trough': avg_trough,
'description': f'三川型態 ({stage}/5 階段)'
})
return {'signals': signals, 'analysis': analysis}
# ============================================================
# TA-Lib 型態偵測 (保持原有功能)
# ============================================================
def detect_talib_patterns(df: pd.DataFrame) -> dict:
"""Detect all TA-Lib candlestick patterns."""
results = {}
open_prices = df['Open'].values
high_prices = df['High'].values
low_prices = df['Low'].values
close_prices = df['Close'].values
for pattern_key, pattern_info in TALIB_PATTERNS.items():
func_name = pattern_key
try:
func = getattr(talib, func_name)
pattern_result = func(open_prices, high_prices, low_prices, close_prices)
results[pattern_key] = {
'values': pattern_result,
'info': pattern_info
}
except Exception as e:
print(f"Warning: Could not compute {pattern_key}: {e}")
return results
# ============================================================
# 其他自訂型態 (簡化版)
# ============================================================
def detect_sanku_v2(df: pd.DataFrame, trend_ctx: dict) -> dict:
"""
改良版三空偵測 (v2.1) - 使用狀態機記錄連續缺口
三空上漲: 連續 3 個向上缺口 -> 空頭信號 (漲勢竭盡)
三空下跌: 連續 3 個向下缺口 -> 多頭信號 (跌勢竭盡)
Returns:
dict: {
'up_signals': 三空上漲信號,
'down_signals': 三空下跌信號,
'sequences': 偵測到的序列詳情
}
"""
up_signals = np.zeros(len(df))
down_signals = np.zeros(len(df))
sequences = []
highs = df['High'].values
lows = df['Low'].values
# 狀態機: 追蹤連續缺口
gap_up_streak = 0
gap_up_start = -1
gap_down_streak = 0
gap_down_start = -1
for i in range(1, len(df)):
# 檢測向上缺口
is_gap_up = lows[i] > highs[i-1]
# 檢測向下缺口
is_gap_down = highs[i] < lows[i-1]
# 向上缺口序列追蹤
if is_gap_up:
if gap_up_streak == 0:
gap_up_start = i
gap_up_streak += 1
# 三空上漲確認
if gap_up_streak >= 3:
up_signals[i] = -100 # 空頭信號
sequences.append({
'type': 'SANKU_UP',
'start_idx': gap_up_start,
'end_idx': i,
'gaps': gap_up_streak,
'signal': 'bearish',
'description': f'連續 {gap_up_streak} 個向上缺口 (漲勢竭盡)'
})
else:
gap_up_streak = 0
gap_up_start = -1
# 向下缺口序列追蹤
if is_gap_down:
if gap_down_streak == 0:
gap_down_start = i
gap_down_streak += 1
# 三空下跌確認
if gap_down_streak >= 3:
down_signals[i] = 100 # 多頭信號
sequences.append({
'type': 'SANKU_DOWN',
'start_idx': gap_down_start,
'end_idx': i,
'gaps': gap_down_streak,
'signal': 'bullish',
'description': f'連續 {gap_down_streak} 個向下缺口 (跌勢竭盡)'
})
else:
gap_down_streak = 0
gap_down_start = -1
return {
'up_signals': up_signals,
'down_signals': down_signals,
'sequences': sequences
}
def detect_tweezers_v2(df: pd.DataFrame, trend_ctx: dict, lookback: int = 20) -> tuple:
"""
改良版鑷子偵測 (v2.1)
關鍵修正: 只有在創下 20 日新高/新低時出現的雙針才算有效鑷子
半山腰或盤整區的雙針視為雜訊直接丟棄
Args:
df: OHLC DataFrame
trend_ctx: 趨勢背景
lookback: 回看天數 (預設 20 )
"""
top_signals = np.zeros(len(df))
bottom_signals = np.zeros(len(df))
highs = df['High'].values
lows = df['Low'].values
tolerance = 0.002 # 0.2% tolerance
for i in range(lookback, len(df)):
# 計算 lookback 期間的最高/最低
recent_high = max(highs[i-lookback:i])
recent_low = min(lows[i-lookback:i])
# ========================================
# 鑷子頂: 需同時滿足以下條件
# 1. 今日高點 ≈ 昨日高點 (容差 0.2%)
# 2. 今日高點 >= 近 20 日最高
# 3. 趨勢位置在高檔或中間
# ========================================
if trend_ctx['position'] in ['high', 'mid']:
two_highs_equal = abs(highs[i] - highs[i-1]) / highs[i] < tolerance
at_20d_high = highs[i] >= recent_high * 0.998 # 允許微小誤差
if two_highs_equal and at_20d_high:
top_signals[i] = -100
# ========================================
# 鑷子底: 需同時滿足以下條件
# 1. 今日低點 ≈ 昨日低點 (容差 0.2%)
# 2. 今日低點 <= 近 20 日最低
# 3. 趨勢位置在低檔或中間
# ========================================
if trend_ctx['position'] in ['low', 'mid']:
two_lows_equal = abs(lows[i] - lows[i-1]) / lows[i] < tolerance
at_20d_low = lows[i] <= recent_low * 1.002 # 允許微小誤差
if two_lows_equal and at_20d_low:
bottom_signals[i] = 100
return top_signals, bottom_signals
def detect_windows(df: pd.DataFrame) -> tuple:
"""Detect Rising/Falling Window (gap) patterns."""
up_signals = np.zeros(len(df))
down_signals = np.zeros(len(df))
highs = df['High'].values
lows = df['Low'].values
for i in range(1, len(df)):
# Rising window (gap up)
if lows[i] > highs[i-1]:
up_signals[i] = 100
# Falling window (gap down)
if highs[i] < lows[i-1]:
down_signals[i] = -100
return up_signals, down_signals
# ============================================================
# 主要入口函數
# ============================================================
def detect_all_patterns(df: pd.DataFrame) -> dict:
"""
偵測所有 80 種酒田型態 (改良版)
Returns:
dict: {
'patterns': DataFrame (每日每型態的信號),
'trend_context': 趨勢背景資訊,
'sanzan_analysis': 三山詳細分析,
'sansen_analysis': 三川詳細分析
}
"""
# 計算趨勢背景
trend_ctx = calculate_trend_context(df)
# 建立結果 DataFrame
results = pd.DataFrame(index=df.index)
# TA-Lib 型態
talib_results = detect_talib_patterns(df)
for pattern_key, pattern_data in talib_results.items():
results[pattern_key] = pattern_data['values']
# 改良版三山/三川
sanzan_result = detect_sanzan_v2(df, trend_ctx)
sansen_result = detect_sansen_v2(df, trend_ctx)
results['SANZAN'] = sanzan_result['signals']
results['SANSEN'] = sansen_result['signals']
# 其他自訂型態 (使用 v2.1 改良版)
sanku_result = detect_sanku_v2(df, trend_ctx)
results['SANKU_UP'] = sanku_result['up_signals']
results['SANKU_DOWN'] = sanku_result['down_signals']
tweezers_top, tweezers_bottom = detect_tweezers_v2(df, trend_ctx)
results['TWEEZERS_TOP'] = tweezers_top
results['TWEEZERS_BOTTOM'] = tweezers_bottom
window_up, window_down = detect_windows(df)
results['WINDOW_UP'] = window_up
results['WINDOW_DOWN'] = window_down
return {
'patterns': results,
'trend_context': trend_ctx,
'sanzan_analysis': sanzan_result['analysis'],
'sansen_analysis': sansen_result['analysis']
}
def get_pattern_info(pattern_key: str) -> dict:
"""Get pattern information by key."""
if pattern_key in TALIB_PATTERNS:
return TALIB_PATTERNS[pattern_key]
elif pattern_key in CUSTOM_PATTERNS:
return CUSTOM_PATTERNS[pattern_key]
return None
def summarize_detected_patterns(df: pd.DataFrame, pattern_results: pd.DataFrame) -> list:
"""
Summarize all detected patterns with dates and signals.
Returns:
List of dicts: [{date, pattern, signal, strength, info}, ...]
"""
detected = []
for col in pattern_results.columns:
non_zero = pattern_results[col] != 0
if non_zero.any():
for idx in pattern_results[non_zero].index:
signal_value = pattern_results.loc[idx, col]
info = get_pattern_info(col)
if info:
detected.append({
'date': idx,
'pattern': col,
'name': info['name'],
'english': info['en'],
'type': info['type'],
'direction': 'bullish' if signal_value > 0 else 'bearish',
'strength': abs(signal_value),
'info': info
})
# Sort by date
detected.sort(key=lambda x: x['date'], reverse=True)
return detected

View File

@ -0,0 +1,14 @@
# Core dependencies
yfinance>=0.2.36
pandas>=2.0.0
numpy>=1.24.0
# Technical Analysis
TA-Lib>=0.4.28
# Charting
mplfinance>=0.12.10b0
matplotlib>=3.7.0
# Utilities
argparse>=1.4.0

View File

@ -0,0 +1,467 @@
#!/usr/bin/env python3
"""
Sakata Analyzer v2.0 - 酒田戰法分析器 (改良版)
Main entry point for candlestick pattern analysis.
改良重點:
1. 整合結構分析 (頸線量價背離)
2. 改進報告格式 (顯示型態階段關鍵價位)
3. 趨勢背景信息
Usage:
python sakata_analyzer.py --ticker AAPL
python sakata_analyzer.py --ticker TSLA --days 180
"""
import argparse
import os
import time
from datetime import datetime, timedelta
import pandas as pd
import yfinance as yf
from pattern_detector import detect_all_patterns, summarize_detected_patterns
from chart_plotter import create_sakata_chart
from signal_generator import SignalGenerator
# Rate limiting settings for Yahoo Finance
REQUEST_DELAY = 1.0 # seconds between requests
MAX_RETRIES = 3
RETRY_DELAY = 5.0 # seconds
def fetch_stock_data(ticker: str, days: int = 120, retries: int = 0) -> pd.DataFrame:
"""
Fetch OHLCV data from Yahoo Finance with rate limiting.
"""
print(f"📊 正在取得 {ticker}{days} 日數據...")
try:
time.sleep(REQUEST_DELAY)
end_date = datetime.now()
start_date = end_date - timedelta(days=days + 30)
stock = yf.Ticker(ticker)
df = stock.history(start=start_date, end=end_date)
if df.empty:
raise ValueError(f"No data found for {ticker}")
df = df.tail(days)
print(f"✅ 成功取得 {len(df)} 筆數據 ({df.index[0].strftime('%Y-%m-%d')} ~ {df.index[-1].strftime('%Y-%m-%d')})")
return df
except Exception as e:
if retries < MAX_RETRIES:
print(f"⚠️ 請求失敗,{RETRY_DELAY} 秒後重試... ({retries + 1}/{MAX_RETRIES})")
time.sleep(RETRY_DELAY * (retries + 1))
return fetch_stock_data(ticker, days, retries + 1)
else:
raise Exception(f"Failed to fetch data for {ticker} after {MAX_RETRIES} retries: {e}")
def generate_structure_section(analysis: dict, pattern_type: str) -> str:
"""
生成結構分析區塊 (頸線量價背離)
"""
if not analysis.get('detected'):
return ""
name = '三山' if pattern_type == 'sanzan' else '三川'
stage = analysis.get('stage', 0)
section = f"""
## 🏔️ {name}型態結構分析
| 項目 | 數值 |
|------|------|
| **型態階段** | {stage}/5 {'⚠️ 預警中' if stage < 4 else '✅ 確立'} |
"""
# 頸線信息
neckline = analysis.get('neckline', {})
if neckline and neckline.get('neckline'):
status = '✅ 已跌破' if neckline.get('confirmed') else '⏳ 未跌破'
if pattern_type == 'sansen':
status = '✅ 已突破' if neckline.get('confirmed') else '⏳ 未突破'
section += f"""| **頸線價位** | ${neckline['neckline']:.2f} |
| **頸線狀態** | {status} |
| **距離頸線** | {neckline.get('distance_pct', 0):.1f}% |
"""
# 量價背離
vol_div = analysis.get('volume_divergence', {})
if vol_div and vol_div.get('divergence'):
section += f"""| **量價背離** | ⚠️ {vol_div['description']} |
"""
# 關鍵價位
if pattern_type == 'sanzan' and analysis.get('peaks'):
avg_peak = analysis.get('avg_peak', 0)
section += f"""| **平均山頂** | ${avg_peak:.2f} |
"""
elif pattern_type == 'sansen' and analysis.get('troughs'):
avg_trough = analysis.get('avg_trough', 0)
section += f"""| **平均谷底** | ${avg_trough:.2f} |
"""
# 型態階段說明
stage_desc = {
1: '僅形成一個頂/底',
2: '形成兩個頂/底',
3: '第三頂/底形成中',
4: '型態成形,等待確認',
5: '型態確立,已突破/跌破頸線'
}
section += f"""
### 階段說明
{stage_desc.get(stage, '未知')}
"""
return section
def generate_trend_section(trend_ctx: dict, current_price: float) -> str:
"""
生成趨勢背景區塊
"""
position_map = {'high': '高檔', 'mid': '中間', 'low': '低檔'}
trend_map = {'uptrend': '上升趨勢 📈', 'downtrend': '下降趨勢 📉', 'sideways': '盤整 ↔️'}
ma20 = trend_ctx.get('ma20', [0])[-1] if len(trend_ctx.get('ma20', [])) > 0 else 0
ma60 = trend_ctx.get('ma60', [0])[-1] if len(trend_ctx.get('ma60', [])) > 0 else 0
section = f"""
## 📊 趨勢背景
| 指標 | 數值 | 說明 |
|------|------|------|
| **當前位置** | {position_map.get(trend_ctx.get('position', 'mid'), '中間')} | 近60日區間位置 ({trend_ctx.get('position_pct', 0)*100:.0f}%) |
| **趨勢方向** | {trend_map.get(trend_ctx.get('trend', 'sideways'), '盤整')} | 基於 MA20 斜率 |
| **MA20** | ${ma20:.2f} | {'股價在上方 ✅' if current_price > ma20 else '股價在下方 ❌'} |
| **MA60** | ${ma60:.2f} | {'股價在上方 ✅' if current_price > ma60 else '股價在下方 ❌'} |
| **近期高點** | ${trend_ctx.get('recent_high', 0):.2f} | |
| **近期低點** | ${trend_ctx.get('recent_low', 0):.2f} | |
"""
return section
def generate_markdown_report(
ticker: str,
df: pd.DataFrame,
detected_patterns: list,
signals: list,
chart_path: str,
analysis_result: dict,
output_dir: str = './output'
) -> str:
"""
Generate an improved Markdown analysis report with structure analysis.
"""
signal_gen = SignalGenerator()
summary = signal_gen.summarize_signals(signals)
recommendation = signal_gen.get_latest_recommendation(
signals,
current_price=df.iloc[-1]['Close'],
current_date=df.index[-1]
)
current_price = df.iloc[-1]['Close']
trend_ctx = analysis_result.get('trend_context', {})
sanzan_analysis = analysis_result.get('sanzan_analysis', {})
sansen_analysis = analysis_result.get('sansen_analysis', {})
# 判斷是否有結構型態
has_structure = sanzan_analysis.get('detected') or sansen_analysis.get('detected')
# Build report
report = f"""# {ticker} 酒田戰法分析報告 v2.0
**生成時間**: {datetime.now().strftime('%Y-%m-%d %H:%M')}
**數據範圍**: {df.index[0].strftime('%Y-%m-%d')} ~ {df.index[-1].strftime('%Y-%m-%d')} ({len(df)} )
**當前價格**: ${current_price:.2f}
---
## 📊 核心結論
| 指標 | 數值 |
|------|------|
| 總信號數 | {summary['total']} |
| 多頭信號 | {summary['bullish']} 🟢 |
| 空頭信號 | {summary['bearish']} 🔴 |
| 整體偏向 | **{summary['bias']}** |
| 信心度 | {'' * int(summary['avg_strength'])} ({summary['avg_strength']}/5) |
"""
# 添加趨勢背景
report += generate_trend_section(trend_ctx, current_price)
# 添加結構分析 (如果有)
if sanzan_analysis.get('detected'):
report += generate_structure_section(sanzan_analysis, 'sanzan')
if sansen_analysis.get('detected'):
report += generate_structure_section(sansen_analysis, 'sansen')
# 交易建議
report += """---
## 🎯 最新交易建議
"""
if recommendation:
# v2.2: 根據狀態決定顯示方式
status = recommendation.get('status', 'ACTIVE')
status_reason = recommendation.get('status_reason', '')
days_diff = recommendation.get('days_since_signal', 0)
# 決定 emoji 和建議文字
if recommendation['recommendation'] == 'HOLD':
rec_emoji = '⏸️ 觀望'
elif recommendation['recommendation'] == 'BUY':
rec_emoji = '🟢 買入'
else:
rec_emoji = '🔴 賣出'
# 狀態標籤
status_badge = ''
if status == 'EXPIRED':
status_badge = '⚠️ 已過期'
elif status == 'STOPPED_OUT':
status_badge = '🛑 已停損'
elif status == 'SPECIAL':
status_badge = '⚡ 特殊情況'
elif status == 'ACTIVE':
status_badge = '✅ 有效'
report += f"""| 項目 | 數值 |
|------|------|
| **建議** | {rec_emoji} |
| **信號狀態** | {status_badge} |
| **觸發型態** | {recommendation['pattern']} |
| **信號日期** | {recommendation['date'].strftime('%Y-%m-%d') if hasattr(recommendation['date'], 'strftime') else recommendation['date']} ({days_diff} 天前) |
| **建議進場** | ${recommendation['entry']:.2f} |
| **停損價位** | ${recommendation['stop_loss']:.2f} |
| **目標價位** | ${recommendation['target']:.2f} |
| **信號強度** | {recommendation['strength']} |
"""
# 狀態原因說明
if status_reason:
report += f"""> **狀態說明**: {status_reason}
"""
# 連續窗口警示
window_status = recommendation.get('window_status', {})
if window_status.get('warning'):
report += f"""> **酒田特殊警示**: {window_status['warning']}
"""
# 只有 ACTIVE 狀態才顯示盈虧比
if status == 'ACTIVE':
risk = abs(recommendation['entry'] - recommendation['stop_loss'])
reward = abs(recommendation['target'] - recommendation['entry'])
rr_ratio = reward / risk if risk > 0 else 0
report += f"""**盈虧比 (R/R)**: 1 : {rr_ratio:.1f}
"""
else:
report += "> ⚠️ 目前無明確交易信號,建議觀望。\n\n"
# 偵測到的型態 (去重後顯示)
report += """---
## 📈 偵測到的型態
| 日期 | 型態 | 英文名稱 | 方向 | 強度 |
|------|------|---------|------|------|
"""
# 去重: 同一天同一型態只顯示一次
seen = set()
unique_patterns = []
for pattern in detected_patterns[:20]:
key = (pattern['date'].strftime('%Y-%m-%d') if hasattr(pattern['date'], 'strftime') else str(pattern['date'])[:10], pattern['name'])
if key not in seen:
seen.add(key)
unique_patterns.append(pattern)
for pattern in unique_patterns[:12]:
date_str = pattern['date'].strftime('%m/%d') if hasattr(pattern['date'], 'strftime') else str(pattern['date'])[:10]
direction = '🟢 多頭' if pattern['direction'] == 'bullish' else '🔴 空頭'
strength = '' * min(5, max(1, pattern['strength'] // 20))
report += f"| {date_str} | {pattern['name']} | {pattern['english']} | {direction} | {strength} |\n"
report += f"""
---
## 📉 K 線圖表
![{ticker} 酒田戰法分析]({os.path.basename(chart_path)})
---
## ⚠️ 風險提示
1. 本分析僅供參考不構成投資建議
2. K 線型態需配合成交量趨勢等其他指標確認
3. **三山/三川型態需跌破/突破頸線才算確立**
4. 請嚴格執行停損紀律
5. 過去表現不代表未來結果
---
*由酒田戰法 Agent Skill v2.0 自動生成*
"""
# Save report
os.makedirs(output_dir, exist_ok=True)
report_path = os.path.join(output_dir, f'{ticker}_sakata.md')
with open(report_path, 'w', encoding='utf-8') as f:
f.write(report)
return report_path
def analyze(ticker: str, days: int = 120, output_dir: str = './output'):
"""
Run full Sakata analysis on a stock.
"""
print(f"\n{'='*60}")
print(f"🏯 酒田戰法分析器 v2.0 - {ticker}")
print(f"{'='*60}\n")
# 1. Fetch data
df = fetch_stock_data(ticker, days)
# 2. Detect patterns (v2.0 - returns rich analysis)
print("\n🔍 正在偵測 K 線型態 (含結構分析)...")
analysis_result = detect_all_patterns(df)
pattern_results = analysis_result['patterns']
trend_ctx = analysis_result['trend_context']
sanzan_analysis = analysis_result['sanzan_analysis']
sansen_analysis = analysis_result['sansen_analysis']
detected_patterns = summarize_detected_patterns(df, pattern_results)
print(f"✅ 偵測到 {len(detected_patterns)} 個型態")
# 顯示結構分析結果
if sanzan_analysis.get('detected'):
print(f" 📉 三山型態: 階段 {sanzan_analysis.get('stage')}/5")
if sansen_analysis.get('detected'):
print(f" 📈 三川型態: 階段 {sansen_analysis.get('stage')}/5")
# 3. Generate signals
print("\n📈 正在產生交易信號...")
signal_gen = SignalGenerator()
signals = signal_gen.generate_signals(df, detected_patterns, trend_ctx)
print(f"✅ 產生 {len(signals)} 個交易信號")
# 4. Create chart
print("\n🎨 正在繪製圖表...")
chart_path = create_sakata_chart(df, detected_patterns, signals, ticker, output_dir)
print(f"✅ 圖表已儲存: {chart_path}")
# 5. Generate report
print("\n📝 正在產生報告...")
report_path = generate_markdown_report(
ticker, df, detected_patterns, signals, chart_path, analysis_result, output_dir
)
print(f"✅ 報告已儲存: {report_path}")
# 6. Summary
summary = signal_gen.summarize_signals(signals)
recommendation = signal_gen.get_latest_recommendation(
signals,
current_price=df.iloc[-1]['Close'],
current_date=df.index[-1]
)
print(f"\n{'='*60}")
print("📊 分析完成!")
print(f"{'='*60}")
print(f" 趨勢位置: {trend_ctx.get('position', 'mid')} | 趨勢方向: {trend_ctx.get('trend', 'sideways')}")
print(f" 多頭信號: {summary['bullish']} | 空頭信號: {summary['bearish']}")
print(f" 整體偏向: {summary['bias']}")
if recommendation:
print(f"\n 🎯 最新建議: {recommendation['recommendation']}")
print(f" 型態: {recommendation['pattern']}")
print(f" 進場: ${recommendation['entry']:.2f}")
print(f" 停損: ${recommendation['stop_loss']:.2f}")
print(f" 目標: ${recommendation['target']:.2f}")
print(f"\n 📁 輸出檔案:")
print(f" {chart_path}")
print(f" {report_path}")
print(f"{'='*60}\n")
return {
'chart': chart_path,
'report': report_path,
'patterns': detected_patterns,
'signals': signals,
'summary': summary,
'analysis': analysis_result
}
def main():
parser = argparse.ArgumentParser(
description='酒田戰法分析器 v2.0 - Sakata Candlestick Pattern Analyzer',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
範例:
python sakata_analyzer.py --ticker AAPL
python sakata_analyzer.py --ticker TSLA --days 180
python sakata_analyzer.py --ticker NVDA --output ./charts
"""
)
parser.add_argument(
'--ticker', '-t',
type=str,
required=True,
help='股票代碼 (例: AAPL, TSLA, 2330.TW)'
)
parser.add_argument(
'--days', '-d',
type=int,
default=120,
help='分析天數 (預設: 120)'
)
parser.add_argument(
'--output', '-o',
type=str,
default='./output',
help='輸出目錄 (預設: ./output)'
)
args = parser.parse_args()
analyze(
ticker=args.ticker.upper(),
days=args.days,
output_dir=args.output
)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,415 @@
"""
Sakata Signal Generator
買賣信號產生器
Generates buy/sell signals with entry prices, stop-loss levels, and risk-reward ratios.
"""
import pandas as pd
import numpy as np
from datetime import datetime
from typing import Dict, List, Optional
class SignalGenerator:
"""Generate trading signals based on detected Sakata patterns."""
def __init__(self, atr_period: int = 14, risk_reward_ratio: float = 2.0):
"""
Initialize the signal generator.
Args:
atr_period: Period for ATR calculation (for stop-loss)
risk_reward_ratio: Target risk-reward ratio
"""
self.atr_period = atr_period
self.risk_reward_ratio = risk_reward_ratio
def calculate_atr(self, df: pd.DataFrame) -> pd.Series:
"""Calculate Average True Range."""
high = df['High']
low = df['Low']
close = df['Close']
tr1 = high - low
tr2 = abs(high - close.shift(1))
tr3 = abs(low - close.shift(1))
tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
atr = tr.rolling(window=self.atr_period).mean()
return atr
def generate_signals(
self,
df: pd.DataFrame,
detected_patterns: List[Dict],
trend_ctx: Dict = None
) -> List[Dict]:
"""
Generate trading signals from detected patterns.
Args:
df: OHLC DataFrame
detected_patterns: List of detected pattern dicts
trend_ctx: 趨勢背景 (v2.1 新增)
Returns:
List of signal dicts with entry, stop-loss, and target prices
"""
signals = []
atr = self.calculate_atr(df)
for pattern in detected_patterns:
date = pattern['date']
if date not in df.index:
continue
idx = df.index.get_loc(date)
current_price = df.iloc[idx]['Close']
current_atr = atr.iloc[idx] if not pd.isna(atr.iloc[idx]) else current_price * 0.02
# Determine signal direction
is_bullish = pattern['direction'] == 'bullish'
# Calculate entry, stop-loss, and target
if is_bullish:
# Bullish signal - buy
entry_price = current_price * 1.005 # Slightly above close
stop_loss = current_price - (current_atr * 1.5) # 1.5x ATR below
risk = entry_price - stop_loss
target_price = entry_price + (risk * self.risk_reward_ratio)
else:
# Bearish signal - sell/short
entry_price = current_price * 0.995 # Slightly below close
stop_loss = current_price + (current_atr * 1.5) # 1.5x ATR above
risk = stop_loss - entry_price
target_price = entry_price - (risk * self.risk_reward_ratio)
# Calculate signal strength (v2.1: 傳入 trend_ctx)
strength = self._calculate_signal_strength(pattern, df, idx, trend_ctx)
signal = {
'date': date,
'pattern': pattern['name'],
'pattern_en': pattern['english'],
'direction': 'BUY' if is_bullish else 'SELL',
'current_price': round(current_price, 2),
'entry_price': round(entry_price, 2),
'stop_loss': round(stop_loss, 2),
'target_price': round(target_price, 2),
'risk': round(abs(risk), 2),
'reward': round(abs(risk) * self.risk_reward_ratio, 2),
'risk_reward': f"1:{self.risk_reward_ratio}",
'strength': strength,
'strength_stars': '' * strength,
'pattern_type': pattern['type'],
'atr': round(current_atr, 2)
}
signals.append(signal)
# Sort by date (newest first) and strength
signals.sort(key=lambda x: (x['date'], -x['strength']), reverse=True)
return signals
def _calculate_signal_strength(
self,
pattern: Dict,
df: pd.DataFrame,
idx: int,
trend_ctx: Dict = None
) -> int:
"""
Calculate signal strength from 1-5.
v2.1 改良: 考慮順勢/逆勢權重
- 順勢 (uptrend + BUY downtrend + SELL): +2
- 逆勢 (downtrend + BUY uptrend + SELL): -1
Factors:
- Pattern reliability (reversal vs continuation)
- Volume confirmation
- Trend alignment (v2.1 改良)
- Pattern clarity (signal value)
"""
strength = 3 # Base strength
# Pattern type bonus
pattern_type = pattern.get('type', '')
if pattern_type == 'reversal':
strength += 1 # Reversal patterns are stronger signals
# Signal clarity
raw_strength = pattern.get('strength', 50)
if raw_strength >= 100:
strength += 1
elif raw_strength <= 30:
strength -= 1
# Volume confirmation (if available)
if 'Volume' in df.columns and idx > 0:
current_vol = df.iloc[idx]['Volume']
avg_vol = df['Volume'].iloc[max(0, idx-20):idx].mean()
if current_vol > avg_vol * 1.5:
strength += 1 # High volume confirmation
# v2.1: 趨勢順逆權重調整
is_bullish = pattern['direction'] == 'bullish'
# 使用傳入的 trend_ctx 或計算 MA20 斜率
if trend_ctx and 'trend' in trend_ctx:
trend = trend_ctx['trend']
elif idx >= 20:
ma20_current = df['Close'].iloc[idx-20:idx].mean()
ma20_prev = df['Close'].iloc[idx-25:idx-5].mean() if idx >= 25 else ma20_current
if ma20_current > ma20_prev * 1.01:
trend = 'uptrend'
elif ma20_current < ma20_prev * 0.99:
trend = 'downtrend'
else:
trend = 'sideways'
else:
trend = 'sideways'
# 順勢/逆勢權重
if trend == 'uptrend':
if is_bullish:
strength += 2 # 順勢多頭: +2
else:
strength -= 1 # 逆勢空頭: -1
elif trend == 'downtrend':
if not is_bullish:
strength += 2 # 順勢空頭: +2
else:
strength -= 1 # 逆勢多頭: -1 (這是最危險的!)
# sideways: 不調整
return max(1, min(5, strength)) # Clamp to 1-5
def check_consecutive_windows(
self,
signals: List[Dict],
lookback_days: int = 5
) -> Dict:
"""
v2.2: 檢測連續同向窗口 (二空/三空)
二空: 連續 2 個同向缺口 -> 強勢警示
三空: 連續 3 個同向缺口 -> 力竭反轉信號
"""
from datetime import timedelta
# 篩選窗口型態
window_signals = [
s for s in signals
if 'Window' in s.get('pattern_en', '') or '窗口' in s.get('pattern', '')
]
if len(window_signals) < 2:
return {'count': 0, 'direction': None, 'warning': None}
# 依日期排序 (新到舊)
window_signals.sort(key=lambda x: x['date'], reverse=True)
# 檢測連續同向缺口 (從最新開始)
consecutive_up = 0
consecutive_down = 0
current_direction = None
max_consecutive_up = 0
max_consecutive_down = 0
for i, sig in enumerate(window_signals[:5]):
sig_direction = sig['direction']
if i == 0:
# 第一個信號確定方向
current_direction = sig_direction
if sig_direction == 'BUY':
consecutive_up = 1
else:
consecutive_down = 1
else:
# 檢查是否同向
if sig_direction == current_direction:
if sig_direction == 'BUY':
consecutive_up += 1
else:
consecutive_down += 1
else:
# 方向改變,停止計數
break
max_consecutive_up = max(max_consecutive_up, consecutive_up)
max_consecutive_down = max(max_consecutive_down, consecutive_down)
max_consecutive = max(max_consecutive_up, max_consecutive_down)
direction = 'UP' if max_consecutive_up > max_consecutive_down else 'DOWN'
warning = None
if max_consecutive == 2:
if direction == 'UP':
warning = '⚠️ 二空上漲中 (強勢軋空,勿追空)'
else:
warning = '⚠️ 二空下跌中 (強勢殺多,勿追多)'
elif max_consecutive >= 3:
if direction == 'UP':
warning = '🔴 三空力竭 (準備賣出)'
else:
warning = '🟢 三空力竭 (準備買入)'
return {
'count': max_consecutive,
'direction': direction,
'warning': warning
}
def get_latest_recommendation(
self,
signals: List[Dict],
current_price: float = None,
current_date = None,
lookback_days: int = 3
) -> Optional[Dict]:
"""
v2.2 實戰邏輯: 取得最新交易建議
改良重點:
1. 時效性: 超過 3 天的信號標記為已過期
2. 停損檢查: 已觸發停損的標記為觀望
3. 二空/三空: 連續窗口序列特殊處理
Args:
signals: List of all signals
current_price: 當前股價
current_date: 當前日期
lookback_days: 信號有效天數 (預設 3 )
Returns:
Recommendation dict with status
"""
from datetime import datetime, timedelta
if not signals:
return None
# 設定當前日期
if current_date is None:
current_date = datetime.now()
elif hasattr(current_date, 'to_pydatetime'):
current_date = current_date.to_pydatetime()
# 依日期排序 (新到舊)
sorted_signals = sorted(signals, key=lambda x: x['date'], reverse=True)
if not sorted_signals:
return None
# 取最新信號
latest_signal = sorted_signals[0]
signal_date = latest_signal['date']
if hasattr(signal_date, 'to_pydatetime'):
signal_date = signal_date.to_pydatetime()
elif isinstance(signal_date, str):
signal_date = datetime.strptime(signal_date[:10], '%Y-%m-%d')
# 計算天數差
days_diff = (current_date - signal_date).days if hasattr(current_date, 'days') == False else 0
try:
days_diff = abs((current_date - signal_date).days)
except:
days_diff = 0
# ========================================
# 1. 時效性檢查
# ========================================
is_expired = days_diff > lookback_days
# ========================================
# 2. 停損觸發檢查
# ========================================
stop_loss_triggered = False
if current_price is not None:
if latest_signal['direction'] == 'SELL':
# 空單: 股價 > 停損價 = 已停損
stop_loss_triggered = current_price > latest_signal['stop_loss']
else:
# 多單: 股價 < 停損價 = 已停損
stop_loss_triggered = current_price < latest_signal['stop_loss']
# ========================================
# 3. 連續窗口檢查 (二空/三空)
# ========================================
window_status = self.check_consecutive_windows(signals)
# ========================================
# 4. 決定狀態和建議
# ========================================
status = 'ACTIVE'
status_reason = None
recommendation = latest_signal['direction']
if stop_loss_triggered:
status = 'STOPPED_OUT'
status_reason = f"股價 ${current_price:.2f} 已觸發停損 ${latest_signal['stop_loss']:.2f}"
recommendation = 'HOLD'
elif is_expired:
status = 'EXPIRED'
status_reason = f"信號已過期 ({days_diff} 天前)"
recommendation = 'HOLD'
elif window_status['warning']:
status = 'SPECIAL'
status_reason = window_status['warning']
# 三空力竭時反向操作
if window_status['count'] >= 3:
recommendation = 'SELL' if window_status['direction'] == 'UP' else 'BUY'
return {
'recommendation': recommendation,
'pattern': latest_signal['pattern'],
'entry': latest_signal['entry_price'],
'stop_loss': latest_signal['stop_loss'],
'target': latest_signal['target_price'],
'strength': latest_signal['strength_stars'],
'date': latest_signal['date'],
'status': status,
'status_reason': status_reason,
'days_since_signal': days_diff,
'window_status': window_status
}
def summarize_signals(self, signals: List[Dict]) -> Dict:
"""
Create a summary of all signals.
"""
if not signals:
return {
'total': 0,
'bullish': 0,
'bearish': 0,
'bias': 'NEUTRAL',
'avg_strength': 0
}
bullish = [s for s in signals if s['direction'] == 'BUY']
bearish = [s for s in signals if s['direction'] == 'SELL']
avg_strength = sum(s['strength'] for s in signals) / len(signals)
if len(bullish) > len(bearish) * 1.5:
bias = 'BULLISH'
elif len(bearish) > len(bullish) * 1.5:
bias = 'BEARISH'
else:
bias = 'NEUTRAL'
return {
'total': len(signals),
'bullish': len(bullish),
'bearish': len(bearish),
'bias': bias,
'avg_strength': round(avg_strength, 1)
}

View File

@ -1,17 +0,0 @@
# ONDS Catalyst Timeline (Jan 29, 2026)
## 🔥 Imminent & High Impact
| Date | Event | Impact | Status |
|------|-------|--------|--------|
| **Jan 28** | **DoD "Blue List" Approval** | 🚀 **Extreme** | **Confirmed**. Optimus Drone approved for US Gov use. Unlocks huge TAM. |
| **Jan 31** | **Border Contract PO** | 🚀 **High** | **Pending**. Initial PO for border protection system expected within Jan. |
| **Mar 11** | **Q4 2025 Earnings** | 🟡 Medium | Estimated. Revenue guidance raised on Jan 16. |
## 📅 Summary
- **Blue List Confirmed**: The biggest regulatory hurdle is cleared. This validates the tech for defense usage.
- **Contract Watch**: All eyes are now on the **Border Protection Contract**. With Blue List approval, the PO is likely imminent (days away).
- **Revenue Outlook**: FY2026 guidance raised to $180M (from $170M).
## 🚦 Trade Implication
- **Bullish Confirmation**: The Blue List news is the "smoking gun" for the bull case.
- **Next Leg Up**: The stock should price in the specific monetary value of the upcoming border contract once announced.

View File

@ -1,26 +0,0 @@
# Entry/Exit Analysis: ONDS (Jan 29, 2026)
## 🎯 Strategy: Catalyst Breakout / Aggressive Trend
**Thesis**: The "Blue List" approval is a fundamental shift. We are no longer buying the *rumor* of safety; we are buying the *fact*. The next leg up is driven by the pending border contract PO. We want to be fully positioned BEFORE that PO drops.
## 🟢 Entry Zones
| Zone | Price Range | Strategy | Confidence |
|------|-------------|----------|------------|
| **Primary (Aggressive)** | **$12.15 - $12.45** | **Market Buy**. Current levels are consolidating nicely above $12. Don't get cute waiting for deep dips. | 🟢 High |
| **Secondary (Dip)** | **$11.85 - $12.00** | **Limit Buy**. If market chops, add heavily here. This is key support. | 🟡 Medium |
| **Breakout Add** | **Above $13.10** | **Momentum Add**. Adding to winners if it clears local resistance with volume. | 🟢 High |
## 🔴 Stop Loss
- **Hard Stop**: **$11.60**
- *Rationale*: A break below $11.60 invalidates the immediate bullish consolidation and suggests the "Blue List" bump is failing or being sold off.
- **Trailing Stop**: Activate at $13.50, trail by $0.50.
## 🏁 Profit Targets
1. **Target 1: $13.50** (Near-term resistance). Take 25% profit here to lock in gains.
2. **Target 2: $15.00** (Contract PO Gap). This is the expected move when the Border Contract is officially announced.
3. **Target 3: $18.00+** (Blue Sky). Longer term hold for full valuation of gov contracts.
## ⚡ Execution Plan
1. **Immediate**: Enter full starter position (50-70% size) at current market (~$12.30).
2. **Watch**: Monitor for the Border Contract PR.
3. **React**: If $11.80 breaks, cut. If $13.50 hits, trim.

View File

@ -1,53 +0,0 @@
# Quick Decision: ONDS (Jan 29, 2026)
## 📊 決策總結
**執行時間**: 2026-01-29 22:20
**標的**: Ondas Holdings (ONDS)
**最終決策**: ✅ **STRONG BUY**
---
## 1⃣ 核心論述 (Why?)
**"Blue List" 認證消除最大風險,邊境合約發布在即。**
- **催化劑 (Catalyst)**: DoD Blue List 批准 (已發生) + 邊境合約 PO (預計本月)。這是從「預期」轉向「落實」的關鍵時刻。
- **基本面 (Fundamental)**: 營收指引上調至 $180M政府背書確保長期營收來源。
- **技術面 (Technical)**: 股價站穩 $12 支撐,呈現多頭排列,回檔即買點。
- **風險分 (Risk Score)**: **90/100** (Critical Priority)
---
## 2⃣ 交易計劃 (Action Plan)
### 💰 買多少 (Position Sizing)
- **建議倉位**: **8-10%** (高信心/高風險配置)
- **資金分配**:
- **第一批 (60%)**: 現價 **$12.30** 直接進場 (Aggressive)。
- **第二批 (40%)**: 掛單 **$11.90** 防守/加碼。
### ⏰ 什麼時候買 (Timing)
- **立即執行**: 消息面 (Blue List) 強度足以支撐現價買入。不要等待完美低點,以免錯失合約發布後的跳空缺口。
### 🚪 出場設定 (Exit)
- **停損 (Stop)**: **$11.60** (跌破支撐與消息面失效)。
- **目標 (Target)**:
- TP1: **$13.50** (前高/短線)
- TP2: **$15.00** (合約發布後預期目標)
---
## 3⃣ 檢查清單 (Final Check)
| 檢查項目 | 結果 | 備註 |
|----------|------|------|
| ✅ 催化劑明確? | YES | Blue List 已過,等待 PO |
| ✅ 風險可控? | YES | 下檔 $11.60,風險回報比 > 3:1 |
| ✅ 資金充裕? | CHECK | 確保總倉位不超標 |
---
## 📝 執行指令
```bash
# 範例掛單指令 (僅供參考)
BUY ONDS LIMIT 12.30 (60% Size)
BUY ONDS LIMIT 11.90 (40% Size)
STOP LOSS 11.60
```

View File

@ -1,28 +0,0 @@
# Risk Score: ONDS (Jan 29, 2026)
## 🛡️ Total Score: 90/100 (🔥 Critical Priority)
### 1. Catalyst Clarity (35/35) - **MAX SCORE**
- **Blue List (Confirmed)**: The "Blue List" approval is a binary risk event that has resolved positively. There is no longer speculation—it is a fact.
- **Contract PO (Imminent)**: With the regulatory gate opened, the purchase order is a logistical formality expected within 48-72 hours.
### 2. Technical Quality (22/25)
- **Support Hold**: Price successfully tested and held $12.00, now consolidating at $12.30.
- **Trend**: Bullish consolidation flag pattern forming after the initial pop.
- **Risk**: Failure to hold $11.80 would be the only technical negative, but sell-pressure is absorbed by news.
### 3. Fundamentals (15/20)
- **Guidance Raised**: FY2026 revenue outlook up to $180M.
- **DoD Backing**: Government validation validates the tech stack's commercial viability.
### 4. Sentiment & Liquidity (18/20)
- **Volume**: Elevated. Market is watching closely.
- **Institutional Interest**: DoD approval acts as a "seal of quality" for institutional entry.
## ⚠️ Key Risks
1. **"Sell the News"?**: There's a small chance of profit-taking on the Blue List news, but the *actual* contract ($$$) hasn't dropped yet. The big run likely happens on the PO.
2. **Delay**: If the PO doesn't drop by Jan 31, impatience could cause a minor drift lower.
## 💡 Conclusion
**Risk is drastically reduced** compared to yesterday. The regulatory gamble is gone. Now it's an execution play.
**Verdict**: **Aggressive Long**.

View File

@ -1,72 +0,0 @@
---
date: 2026-01-29
ticker: ONDS
type: stock-data
analyst: AI Assistant
---
# 📊 ONDS 股票數據報告 (Jan 29 Update)
**生成時間**: 2026-01-29 21:40
**數據截至**: 2026-01-29 盤中
---
## 📌 快速摘要 ("Blue List" Catalyst)
| 項目 | 數值 | 狀態 | 變化 |
|------|------|------|------|
| 當前股價 | **$12.30** | ✅ | 持穩於 $12 支撐之上 (+0.3%) |
| 催化劑更新 | **Blue List** | 🔥 | Optimus 無人機獲美國防部核准 |
| 預期催化劑 | 邊境合約 | ⏳ | 倒數 48 小時 (仍預期 1 月底) |
| 期權 OI | Call主導 | ✅ | $13 Call OI 增加至 1.8萬口 |
| 成交量 | 100M+ | 🔥 | 連續兩日爆量,換手積極 |
---
## 1⃣ 基礎市場數據 (Consolidation)
- **股價表現**: $12.30 (Day Range: $12.06 - $13.31)
- **技術型態**: 昨日大漲後今日高檔震盪,成功守住 **$12.00** 關卡。
- **成交量**: 100.93M (量縮整理,但仍維持高水位)。
- **市值**: $5.19B。
---
## 2⃣ 重大消息 (Confirmed Catalyst)
### 🎖️ Optimus Drone 獲 "Blue List" 認證
- **內容**: 子公司 American Robotics 的 Optimus 無人機被列入美國國防部 (DoD) 的「藍名單」(Blue UAS Cleared List)。
- **意義**:
1. **合規性**: 符合 NDAA 標準,可供美國政府與軍方採購。
2. **加速採購**: 政府機構可跳過繁瑣審查直接下單。
3. **前兆**: 這極有可能是「邊境保護合約」的前置作業完成訊號。
---
## 3⃣ 期權籌碼 (Jan 30 明日到期)
### 關鍵戰場: $13.00
- **Call OI ($13.00)**: **18,780 口** (最大壓力/目標)
- **Put OI ($12.00)**: 11,707 口 (強力支撐)
- **IV**: 127% (極高,隱含波動劇烈)
- **解讀**: 多頭試圖在明日結算前攻克 $13若成功將引發 Gamma Squeeze。
---
## 4⃣ 綜合評估
### 投資論述 Updated
- **利多堆疊**: 營收指引上調 -> 股價反彈 -> **Blue List 認證 (今日)** -> 邊境合約 (預期明日/後天)。
- **ז機**: "Blue List" 是極強的基本面背書,大幅降低了合約落空的風險。
- **風險**: 1/31 若無合約公告,短線可能因預期落空回測 $11。
### 交易策略
- **持有者**: 續抱Blue List 消息證實公司在政府端的進展順利。
- **空手者**: **$12.00 - $12.30** 仍是買點。
- **目標**: 短線 $13.50,合約發布後看 $15+。
---
## 🔗 後續步驟
1. `/catalyst-check ONDS` - 雖然已確認 Blue List但仍需緊盯邊境合約公告。

View File

@ -1,64 +0,0 @@
# 分析 Session: 2026-01-29
**建立時間**: 2026-01-29 20:41
**狀態**: 🟡 進行中
---
## 📋 分析標的
| Ticker | 優先級 | 數據 | 催化劑 | 風險 | 進出場 |
|--------|--------|------|--------|------|--------|
| ONDS | **90** (🔥) | ✅ | ✅ | ✅ | ✅ |
圖例: ⬜ 未完成 | 🟡 進行中 | ✅ 已完成
---
## 📊 篩選結果
> 執行 `/stock-scan` 後自動填入
---
## 📈 個股分析
### ONDS (Jan 29 Update)
#### 基礎數據
- **股價**: $12.30 (持穩)
- **新消息**: **Optimus Drone 獲 DoD "Blue List" 認證** 🔥
- **解讀**: 政府採購綠燈,邊境合約發布機率大增。
- [完整報告](2026-01-29-ONDS-stock-data.md)
#### 催化劑
- **✅ Blue List**: DoD 認證已獲批 (Jan 28)。
- **🚀 邊境合約**: 預期 Jan 31 前發布 PO。
- **Earnings**: 預計 Mar 11。
- [完整報告](2026-01-29-ONDS-catalysts.md)
#### 風險評分
- **分數**: **90/100** (🔥 Critical)
- **變動**: 從 87 升至 90 (因 Blue List 確認)。
- **觀點**: 管制風險消除,專注執行面。
- [完整報告](2026-01-29-ONDS-risk-score.md)
#### 進出場
- **Buy Zone**: **$12.15 - $12.45** (Aggressive/Market)
- **Stop**: $11.60 (Strict)
- **Targets**: $13.50 (Short) / $15.00 (Contract)
- [完整報告](2026-01-29-ONDS-entry-exit.md)
---
## 📝 手動筆記
[在此添加任何觀察或備註]
---
## 🔄 執行紀錄
| 時間 | Workflow | 標的 | 備註 |
|------|----------|------|------|
| 22:25 | `/entry-exit` | ONDS | Buy @ $12.15-$12.45. Stop $11.60. [報告](2026-01-29-ONDS-entry-exit.md) |

View File

@ -0,0 +1,208 @@
---
date: 2026-01-30
ticker: ONDS
type: stock-data
---
# 📊 ONDS 股票數據報告
## 📌 快速摘要 (5 指標)
| 項目 | 數值 | 評級 |
|------|------|------|
| 股價/市值 | $0.75-0.85 / $55-65M | ⚠️ |
| IV / OI | 待查 / 待查 | ❌ |
| 機構態度 | 持股 15-18% | ⚠️ |
| 財務狀況 | 待查 (小型股高風險) | ⚠️ |
| 新聞情緒 | 待查 | ❌ |
> ⚠️ 部分數據因網路搜尋限制,標記為推測或待查
---
## 1⃣ 基礎市場數據
| 指標 | 數值 | 狀態 |
|------|------|------|
| **當前股價** | $0.75 - $0.85 | ⚠️ 推測 (May 2024 數據) |
| **市值** | $55M - $65M | ⚠️ 推測 |
| **52週區間** | $0.60 - $1.88 | ⚠️ 推測 |
| **日均成交量** | 待查 | ❌ |
| **流通股數** | ~75M 股 (估算) | ⚠️ 推測 |
| **機構持股比例** | 15% - 18% | ⚠️ 推測 |
**來源**: Yahoo Finance (歷史數據), SEC 13F
---
## 2⃣ 公司業務概覽
### Ondas Holdings Inc. (NASDAQ: ONDS)
**總部**: Waltham, Massachusetts
### 雙主軸業務:
#### 🛜 Ondas Networks (無線網路技術)
- **核心產品**: FullMAX 軟體定義無線電 (SDR) 平台
- **技術標準**: IEEE 802.16s
- **特點**: 高可靠性、長距離、低延遲
- **目標市場**:
- Class I 鐵路 (自動列車控制)
- 電力公用事業
- 石油天然氣
#### 🚁 Ondas Autonomous Systems (OAS)
- **核心產品**: Drone-in-a-Box 自動化無人機系統
- **關鍵併購**: American Robotics + Airobotics
- **產品線**:
- **Optimus System** (Airobotics): 工業/城市全自動無人機
- **Scout System** (American Robotics): 首個獲 FAA BVLOS 批准的無人機
- **應用場景**:
- 智慧城市 (緊急響應、公共安全)
- 工業監控 (礦場、煉油廠、港口)
- 國防 (戰術情報)
**戰略整合**: FullMAX 無線技術 + 自動無人機 = 端到端生態系統
---
## 3⃣ 機構持股
| 指標 | 數值 |
|------|------|
| **總機構持股** | ~11-13M 股 |
| **佔流通股** | 15-18% |
| **活躍機構數** | 40-50 家 |
### Top 5 機構 (推測)
| 機構 | 持股狀態 | 評分 |
|------|----------|------|
| Vanguard Group | 最大持股方 (>2M 股) | 🟢 |
| BlackRock Inc. | 主要持股 | 🟢 |
| Geode Capital | 穩定持有 | 🟡 |
| State Street | Top 5 | 🟡 |
| Renaissance Technologies | 活躍交易 | 🟡 |
**態度**: ⚠️ 中性 (低機構持股比例,高波動風險)
**來源**: SEC 13F (Q4 2024 數據)
---
## 4⃣ 財務數據
| 指標 | 數值 | 狀態 |
|------|------|------|
| **年營收** | ~$15-20M | ⚠️ 推測 |
| **毛利率** | 待查 | ❌ |
| **現金** | 待查 | ❌ |
| **總債務** | 待查 | ❌ |
| **EPS** | 預計虧損 | ⚠️ 推測 |
**財務狀況**: ⚠️ 高風險小型股,需查閱最新 10-Q/10-K
---
## 5⃣ 期權市場
| 指標 | 數值 | 狀態 |
|------|------|------|
| **IV** | 待查 | ❌ |
| **IV Rank** | 待查 | ❌ |
| **Call/Put OI** | 待查 | ❌ |
| **Call Wall** | 待查 | ❌ |
| **Put Wall** | 待查 | ❌ |
> ❌ 小型股期權數據可能流動性不足
---
## 6⃣ 分析師評級
| 指標 | 數值 | 狀態 |
|------|------|------|
| **目標價範圍** | $1.50 - $4.00 | ⚠️ 推測 |
| **評級共識** | 待查 | ❌ |
---
## 7⃣ 基本面評估
### 成長驅動
1. **無人機智慧城市合約** - 中東擴張 (杜拜、阿布達比)
- 時間: 持續進行中 | 影響: 🟢 高
2. **鐵路無線網路** - 北美 Class I 鐵路部署
- 時間: 持續進行中 | 影響: 🟢 高
3. **BVLOS 監管優勢** - Scout 系統獨特 FAA 批准
- 時間: 已獲批 | 影響: 🟡 中
### 主要風險
| 風險 | 嚴重性 | 描述 |
|------|--------|------|
| 財務風險 | 🔴 高 | 小型股現金消耗,可能需融資/稀釋 |
| 執行風險 | 🟠 中 | 商業化進度不確定 |
| 競爭風險 | 🟠 中 | 無人機/無線市場競爭激烈 |
| 流動性風險 | 🔴 高 | 低機構持股,高波動 |
### 投資論述
**為什麼買?**
- 雙主軸業務 (無線網路 + 無人機) 具備協同效應
- FAA BVLOS 批准形成監管護城河
- 智慧城市/工業自動化趨勢受益者
**為什麼現在?**
- 需確認具體催化劑時間表
**上行空間**
- 分析師目標價 $1.50-$4.00 (當前 ~$0.80)
- 若成功商業化,市值可達 $200M+
**下行風險**
- 現金消耗 -> 稀釋風險
- 商業化進度延遲
- 整體市場對小型成長股不友善
### 決策建議
**適合類型**:
- ⚠️ 高風險投機 (小型股,低機構持股)
- ⚠️ 事件驅動 (需確認催化劑)
**決策**: ⚠️ **觀察等待** - 需補充以下資訊:
1. 最新季報財務數據 (現金、燒錢率)
2. 具體合約/催化劑時間表
3. 期權市場結構
---
## 📊 數據完整度
| 項目 | 狀態 |
|------|------|
| 基礎市場 | ⚠️ 推測 |
| 財務數據 | ❌ 遺漏 |
| 機構持股 | ⚠️ 推測 |
| 期權市場 | ❌ 遺漏 |
| 公司業務 | ✅ 已確認 |
| 風險評估 | ⚠️ 推測 |
**整體完整度**: ~50%
---
## 🔗 後續步驟
1. `/catalyst-check ONDS` - 催化劑詳析
2. `/risk-score ONDS` - 風險評分
3. `/entry-exit ONDS` - 進出場點
**手動補充建議**:
- 查閱 [SEC EDGAR](https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=ONDS) 最新 10-Q/10-K
- 查閱 [Yahoo Finance](https://finance.yahoo.com/quote/ONDS) 即時報價
- 查閱 [Fintel](https://fintel.io/so/us/onds) 機構持股詳情
---
**更新時間**: 2026-01-30 16:56
**分析師**: AI Agent
**數據標註**: ✅已確認 | ⚠️推測 | ❌遺漏

60
sessions/2026-01-30.md Normal file
View File

@ -0,0 +1,60 @@
# 分析 Session: 2026-01-30
**建立時間**: 2026-01-30 16:55
**狀態**: 🟡 進行中
---
## 📋 分析標的
| Ticker | 優先級 | 數據 | 催化劑 | 風險 | 進出場 |
|--------|--------|------|--------|------|--------|
| ONDS | ⚠️ 中 | 🟡 | ⬜ | ⬜ | ⬜ |
圖例: ⬜ 未完成 | 🟡 進行中 | ✅ 已完成
---
## 📊 篩選結果
> 手動指定: ONDS (Ondas Holdings)
---
## 📈 個股分析
### ONDS (Ondas Holdings)
#### 基礎數據
- **股價**: $0.75-0.85 ⚠️
- **市值**: $55-65M ⚠️
- **機構持股**: 15-18% ⚠️
- **業務**: 無線網路 (FullMAX) + 自動無人機 (Drone-in-a-Box)
> 詳見: [2026-01-30-ONDS-stock-data.md](./2026-01-30-ONDS-stock-data.md)
#### 催化劑
> 執行 `/catalyst-check ONDS` 後填入
#### 風險評分
> 執行 `/risk-score ONDS` 後填入
#### 進出場
> 執行 `/entry-exit ONDS` 後填入
---
## 📝 手動筆記
- ONDS 是高風險小型股,機構持股偏低
- 雙主軸業務具備協同效應潛力
- 需補充財務數據和催化劑時間表
---
## 🔄 執行紀錄
| 時間 | Workflow | 標的 | 備註 |
|------|----------|------|------|
| 16:55 | /new-session | - | Session 建立 |
| 16:56 | /stock-data | ONDS | 完成 ~50% (部分數據遺漏) |