add job logic
This commit is contained in:
parent
2655fd3eb8
commit
0db2cfaeae
|
|
@ -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"]
|
||||
|
|
@ -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 線圖表
|
||||

|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 型態參考文檔
|
||||
|
||||
當需要解釋特定型態時,參考:
|
||||
- [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. 最終建議需考慮**風險報酬比**
|
||||
|
|
@ -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 線圖表
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 風險提示
|
||||
|
||||
1. 本分析僅供參考,不構成投資建議
|
||||
2. K 線型態需配合成交量、趨勢等其他指標確認
|
||||
3. **三山/三川型態需跌破/突破頸線才算確立**
|
||||
4. 請嚴格執行停損紀律
|
||||
5. 過去表現不代表未來結果
|
||||
|
||||
---
|
||||
|
||||
*由酒田戰法 Agent Skill v2.0 自動生成*
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.7 MiB |
|
|
@ -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)
|
||||
|
|
@ -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. 其他變體
|
||||
相同低價、家鴿、陷阱、改良陷阱、二隻烏鴉等。
|
||||
|
|
@ -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%
|
||||
**意義**: 強勁賣盤
|
||||
|
|
@ -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. 跳空相關型態
|
||||
跳空並列三法、跳空缺口等。
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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 線圖表
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 📚 型態類型說明
|
||||
|
||||
| 類型 | 說明 |
|
||||
|------|------|
|
||||
| 反轉型態 | 趨勢可能即將改變,需配合其他指標確認 |
|
||||
| 延續型態 | 當前趨勢可能持續,可順勢操作 |
|
||||
| 中性型態 | 市場猶豫,建議觀望 |
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 風險提示
|
||||
|
||||
1. 本分析僅供參考,不構成投資建議
|
||||
2. K 線型態需配合成交量、趨勢等其他指標確認
|
||||
3. 請嚴格執行停損紀律
|
||||
4. 過去表現不代表未來結果
|
||||
|
||||
---
|
||||
|
||||
*由酒田戰法 Agent Skill 自動生成*
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.2 MiB |
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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 線圖表
|
||||
|
||||
})
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 風險提示
|
||||
|
||||
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()
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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**.
|
||||
|
|
@ -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,但仍需緊盯邊境合約公告。
|
||||
|
|
@ -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) |
|
||||
|
|
@ -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
|
||||
**數據標註**: ✅已確認 | ⚠️推測 | ❌遺漏
|
||||
|
|
@ -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% (部分數據遺漏) |
|
||||
Loading…
Reference in New Issue