From 6942b4172c66554f42c3d4e1f79e051686d2fc42 Mon Sep 17 00:00:00 2001 From: EagleF6432614 Date: Wed, 28 Jan 2026 16:36:41 +0800 Subject: [PATCH] Add stock-master skill: beginner-friendly stock technical analysis tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Features: - Hybrid data sources (Yahoo Finance + Alpha Vantage MCP) - Easy-to-understand analysis reports for beginners - RSI, MACD, KDJ, Bollinger Bands, OBV indicators - K-line and trend pattern recognition - Buy/sell signals with position suggestions - Excel portfolio management - Feishu (飞书) multi-table sync - Supports US, HK, and A-share markets --- .claude-plugin/marketplace.json | 9 + skills/stock-master/.gitignore | 47 + skills/stock-master/LICENSE | 21 + skills/stock-master/README.md | 203 ++ skills/stock-master/SKILL.md | 135 ++ skills/stock-master/config.example.json | 6 + .../stock-master/feishu_config.example.json | 16 + skills/stock-master/references/changelog.md | 72 + .../stock-master/references/feishu-guide.md | 225 +++ .../references/investment-wisdom.md | 157 ++ skills/stock-master/references/mcp-tools.md | 93 + .../references/portfolio-guide.md | 152 ++ .../stock-master/references/scripts-guide.md | 225 +++ skills/stock-master/requirements.txt | 24 + .../stock-master/scripts/beginner_analyzer.py | 925 +++++++++ .../scripts/feishu_init_tables.py | 228 +++ skills/stock-master/scripts/feishu_sync.py | 492 +++++ skills/stock-master/scripts/indicators.py | 1783 +++++++++++++++++ skills/stock-master/scripts/main.py | 827 ++++++++ skills/stock-master/scripts/portfolio.py | 424 ++++ 20 files changed, 6064 insertions(+) create mode 100644 skills/stock-master/.gitignore create mode 100644 skills/stock-master/LICENSE create mode 100644 skills/stock-master/README.md create mode 100644 skills/stock-master/SKILL.md create mode 100644 skills/stock-master/config.example.json create mode 100644 skills/stock-master/feishu_config.example.json create mode 100644 skills/stock-master/references/changelog.md create mode 100644 skills/stock-master/references/feishu-guide.md create mode 100644 skills/stock-master/references/investment-wisdom.md create mode 100644 skills/stock-master/references/mcp-tools.md create mode 100644 skills/stock-master/references/portfolio-guide.md create mode 100644 skills/stock-master/references/scripts-guide.md create mode 100644 skills/stock-master/requirements.txt create mode 100644 skills/stock-master/scripts/beginner_analyzer.py create mode 100644 skills/stock-master/scripts/feishu_init_tables.py create mode 100644 skills/stock-master/scripts/feishu_sync.py create mode 100644 skills/stock-master/scripts/indicators.py create mode 100644 skills/stock-master/scripts/main.py create mode 100644 skills/stock-master/scripts/portfolio.py diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 1538e00f..5882092a 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -40,6 +40,15 @@ "./skills/web-artifacts-builder", "./skills/webapp-testing" ] + }, + { + "name": "stock-master", + "description": "Beginner-friendly stock technical analysis tool with hybrid data sources (Yahoo Finance + Alpha Vantage MCP). Provides easy-to-understand analysis reports, buy/sell signals, Excel portfolio management, and Feishu sync. Supports US, HK, and A-share markets.", + "source": "./", + "strict": false, + "skills": [ + "./skills/stock-master" + ] } ] } diff --git a/skills/stock-master/.gitignore b/skills/stock-master/.gitignore new file mode 100644 index 00000000..b95bf96a --- /dev/null +++ b/skills/stock-master/.gitignore @@ -0,0 +1,47 @@ +# 敏感配置文件 (包含 API keys) +config.json +feishu_config.json +*_config.json +!*_config.example.json + +# 个人数据 +*.xlsx +*.xls +my_portfolio* + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +.env +.venv +venv/ +ENV/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*.sublime-* + +# macOS +.DS_Store +.AppleDouble +.LSOverride + +# Windows +Thumbs.db +ehthumbs.db +Desktop.ini + +# Logs +*.log +logs/ + +# Testing +.pytest_cache/ +.coverage +htmlcov/ diff --git a/skills/stock-master/LICENSE b/skills/stock-master/LICENSE new file mode 100644 index 00000000..f79615c1 --- /dev/null +++ b/skills/stock-master/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 eaglef6432614 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/stock-master/README.md b/skills/stock-master/README.md new file mode 100644 index 00000000..ea699598 --- /dev/null +++ b/skills/stock-master/README.md @@ -0,0 +1,203 @@ +# Stock Master - 小白友好的股票技术分析工具 + +[![GitHub stars](https://img.shields.io/github/stars/EagleF6432614/stock-master?style=social)](https://github.com/EagleF6432614/stock-master) +[![GitHub forks](https://img.shields.io/github/forks/EagleF6432614/stock-master?style=social)](https://github.com/EagleF6432614/stock-master/fork) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +面向普通投资者的技术分析工具,用日常语言解释复杂指标,给出明确买卖建议。 + +## 特性 + +- **小白友好**: 用通俗语言解释 RSI、MACD、KDJ 等指标 +- **混合数据源**: Yahoo Finance (实时) + Alpha Vantage MCP (专业指标) +- **多市场支持**: 美股、港股、A股 +- **持仓管理**: Excel 表格管理,自动计算盈亏 +- **飞书同步**: 支持同步到飞书多维表格 +- **形态识别**: K线形态 + 趋势形态自动识别 + +## 安装方式 + +### 方式一:Claude Code Marketplace(推荐) + +在 Claude Code 中运行: +```bash +# 1. 添加 marketplace +/plugin marketplace add EagleF6432614/stock-master + +# 2. 安装 skill +/plugin install stock-master@stock-master-marketplace +``` + +安装后直接使用: +``` +分析 AAPL 股票 +``` + +### 方式二:手动安装 + +将仓库克隆到 Claude skills 目录: +```bash +git clone https://github.com/EagleF6432614/stock-master.git ~/.claude/skills/stock-master +``` + +--- + +## 快速开始 + +### 1. 安装依赖 + +```bash +pip install -r requirements.txt +``` + +### 2. 配置 Alpha Vantage MCP (可选) + +在 Claude Code 中添加 MCP 服务器: + +```bash +claude mcp add Alpha-Vantage --transport http --url "https://mcp.alphavantage.co/mcp?apikey=YOUR_API_KEY" +``` + +> 获取免费 API Key: https://www.alphavantage.co/support/#api-key + +### 3. 作为 Claude Skill 使用 + +将此目录复制到 `~/.claude/skills/stock-master`,然后在 Claude Code 中: + +``` +用户: 分析 AAPL 股票 +用户: 看看特斯拉能买吗 +用户: 对比 NVDA 和 AMD +``` + +## 配置说明 + +### 基础配置 + +复制配置模板: + +```bash +cp config.example.json config.json +cp feishu_config.example.json feishu_config.json # 如需飞书同步 +``` + +编辑 `config.json`: + +```json +{ + "portfolio_path": "./my_portfolio.xlsx", + "feishu_config_path": "./feishu_config.json" +} +``` + +### 飞书配置 (可选) + +如需同步到飞书多维表格,编辑 `feishu_config.json`: + +```json +{ + "APP_ID": "cli_xxx", + "APP_SECRET": "xxx", + "APP_TOKEN": "xxx", + "TABLE_ID": "tblxxx" +} +``` + +> 详细配置指南: [飞书开放平台文档](https://open.feishu.cn/document/server-docs/docs/bitable-v1/bitable-overview) + +## 支持的指标 + +| 指标 | 数据源 | 说明 | +|------|--------|------| +| RSI | Alpha Vantage / 本地 | 相对强弱指数 | +| MACD | 本地计算 | 趋势动量指标 | +| KDJ | 本地计算 | 随机指标 | +| 布林带 | Alpha Vantage / 本地 | 波动区间 | +| OBV | 本地计算 | 量能指标 | +| ATR | 本地计算 | 动态止损 | +| 斐波那契 | 本地计算 | 支撑阻力位 | + +## 形态识别 + +### K线形态 +- 锤子线、上吊线 +- 看涨/看跌吞没 +- 早晨之星、黄昏之星 +- 三只白兵、三只乌鸦 +- 十字星 + +### 趋势形态 +- 双底 (W底)、双顶 (M头) +- 头肩底、头肩顶 +- 上升三角形、下降三角形 + +## 交易建议评分 + +| 分数 | 建议 | 仓位 | +|------|------|------| +| ≥6 | 强烈买入 | 30% | +| 3-5 | 建议买入 | 20% | +| -2~2 | 观望 | - | +| -3~-5 | 建议卖出 | - | +| ≤-6 | 强烈卖出 | - | + +## 项目结构 + +``` +stock-master/ +├── SKILL.md # Claude Skill 定义 +├── README.md # 本文件 +├── LICENSE # MIT 许可证 +├── requirements.txt # Python 依赖 +├── config.example.json # 配置模板 +├── feishu_config.example.json # 飞书配置模板 +├── scripts/ +│ ├── main.py # 主分析器 +│ ├── indicators.py # 技术指标计算 +│ ├── beginner_analyzer.py # 小白友好报告生成 +│ ├── portfolio.py # 持仓管理 +│ ├── feishu_sync.py # 飞书同步 +│ └── feishu_init_tables.py # 飞书表结构初始化 +└── references/ + ├── investment-wisdom.md # 投资智慧与哲学 + ├── portfolio-guide.md # Excel 持仓管理指南 + ├── feishu-guide.md # 飞书同步详细指南 + ├── mcp-tools.md # MCP 工具使用指南 + ├── scripts-guide.md # 脚本详细说明 + └── changelog.md # 更新日志 +``` + +## 详细文档 + +| 文档 | 说明 | +|------|------| +| [投资智慧](references/investment-wisdom.md) | 股民十大境界、交易原则、心态修炼 | +| [持仓管理](references/portfolio-guide.md) | Excel 持仓表格使用指南 | +| [飞书同步](references/feishu-guide.md) | 飞书多维表格配置与同步 | +| [MCP 工具](references/mcp-tools.md) | Alpha Vantage API 调用指南 | + +## 风险提示 + +> **免责声明**: 本工具仅供学习和参考,不构成投资建议。 +> +> - 股市有风险,投资需谨慎 +> - 技术分析不能保证盈利 +> - 请根据自身情况做出决策 + +## 贡献 + +欢迎提交 Issue 和 Pull Request! + +## 许可证 + +MIT License - 详见 [LICENSE](LICENSE) + +## 致谢 + +- [yfinance](https://github.com/ranaroussi/yfinance) - Yahoo Finance 数据 +- [Alpha Vantage](https://www.alphavantage.co/) - 专业技术指标 +- [Claude Code](https://claude.ai) - AI 编程助手 + +--- + +如果觉得有用,请给个 Star ⭐ 支持一下! diff --git a/skills/stock-master/SKILL.md b/skills/stock-master/SKILL.md new file mode 100644 index 00000000..79753f29 --- /dev/null +++ b/skills/stock-master/SKILL.md @@ -0,0 +1,135 @@ +--- +name: stock-master +description: 综合性股票技术分析工具,小白友好。采用混合数据源(Yahoo Finance + Alpha Vantage MCP),提供通俗易懂的分析报告、买卖点建议、Excel持仓管理。支持港股本地计算、ATR动态止损、KDJ随机指标、MACD/RSI背离检测、OBV量能分析、斐波那契支撑阻力位、K线形态识别(锤子线/吞没/十字星)、趋势形态识别(双底/头肩/三角形)。支持飞书多维表格同步。当用户请求股票分析、技术指标、交易建议、持仓分析时激活。 +--- +# Stock Master v3.5 - 小白友好版 + +面向普通投资者的技术分析工具,用日常语言解释指标,给出明确买卖建议。 + +> **GitHub**: https://github.com/EagleF6432614/stock-master +> +> 如果觉得有用,请给个 Star ⭐ + +## 快速参考 + +### 数据源策略 +| 数据 | 美股 | 港股/A股 | +|------|------|----------| +| 价格 | Yahoo Finance | Yahoo Finance | +| RSI/布林带 | Alpha Vantage MCP | 本地计算 | +| MACD/KDJ/ATR/均线/OBV/形态 | 本地计算 | 本地计算 | + +### 指标小白解读 + +**RSI** (相对强弱): +- <30 超卖 → "大甩卖,可能捡便宜" +- >70 超买 → "被抢购一空,小心回调" + +**MACD**: +- 金叉 → "踩油门加速,买入信号" +- 死叉 → "松油门减速,卖出警告" + +**KDJ** (随机指标): +- 金叉/J<0 → "绿灯亮了,短期买入" +- 死叉/J>100 → "红灯亮了,短期卖出" + +**布林带**: +- 跌破下轨 → "橡皮筋拉太长,可能反弹" +- 突破上轨 → "涨过头了,可能回落" + +**背离信号**: +- 底背离 → "价格创新低但动能减弱,反弹信号" +- 顶背离 → "价格创新高但动能减弱,回调信号" + +**K线形态** [v3.4]: +- 锤子线/早晨之星/看涨吞没 → 底部反转信号 +- 上吊线/黄昏之星/看跌吞没 → 顶部反转信号 +- 三只白兵/三只乌鸦 → 强趋势确认 + +**趋势形态** [v3.4]: +- 双底(W底)/头肩底 → 底部反转,看涨 +- 双顶(M头)/头肩顶 → 顶部反转,看跌 +- 上升三角形 → 通常向上突破 +- 下降三角形 → 通常向下突破 + +### 交易建议评分 (v3.4) +| 分数 | 建议 | 仓位 | +|------|------|------| +| ≥6 | 强烈买入 | 30% | +| 3-5 | 建议买入 | 20% | +| -2~2 | 观望 | - | +| -3~-5 | 建议卖出 | - | +| ≤-6 | 强烈卖出 | - | + +### 形态信号权重 [v3.4] +| 形态类型 | 权重 | +|----------|------| +| 三只白兵/乌鸦、早晨/黄昏之星、头肩、双底双顶 | ±3 | +| 看涨/看跌吞没、上升/下降三角形 | ±2 | +| 锤子线、十字星等单K线形态 | ±1 | + +## 使用流程 + +### 1. 分析单只股票 +``` +用户: "分析 TSLA" / "看看苹果" / "0700.HK 能买吗" +``` + +**执行步骤**: +1. 获取 Yahoo Finance 实时价格和历史数据 +2. 美股: 调用 Alpha Vantage MCP 获取 RSI/布林带 +3. 港股/A股或API失败: 使用 `scripts/indicators.py` 本地计算 +4. 计算全部指标 (RSI/MACD/KDJ/OBV/背离/支撑阻力/形态) +5. 调用 `scripts/beginner_analyzer.py` 生成小白友好报告 + +### 2. 持仓管理 +``` +用户: "创建持仓表格" / "分析我的持仓" / "更新持仓价格" +``` + +**Excel 路径**: 可在 `config.json` 中配置,默认为当前目录下的 `my_portfolio.xlsx` + +### 3. 对比多只股票 +``` +用户: "对比 AAPL 和 GOOGL" / "这几只哪个好: TSLA, NVDA" +``` + +### 4. 飞书同步 [v3.5] +``` +用户: "同步分析结果到飞书" / "更新飞书持仓" +``` + +**飞书多维表格配置**: 请在 `feishu_config.json` 中配置你的飞书应用凭证和表格 ID。 + +**同步逻辑**: 本地分析 → 飞书(单向,飞书为主库) + +## 配置说明 + +使用前请创建 `config.json` 文件(参考 `config.example.json`): + +```json +{ + "portfolio_path": "./my_portfolio.xlsx", + "feishu_config_path": "./feishu_config.json" +} +``` + +## 风险提示(必须包含) + +每份报告必须包含: +1. 仅供参考,不构成投资建议 +2. 股市有风险,投资需谨慎 +3. 建议分批建仓,设置止损 + +## 详细文档 + +- 投资智慧与哲学: [references/investment-wisdom.md](references/investment-wisdom.md) +- Excel 持仓管理: [references/portfolio-guide.md](references/portfolio-guide.md) +- 飞书同步指南: [references/feishu-guide.md](references/feishu-guide.md) +- MCP 工具调用: [references/mcp-tools.md](references/mcp-tools.md) +- 脚本详细说明: [references/scripts-guide.md](references/scripts-guide.md) +- 更新日志: [references/changelog.md](references/changelog.md) + +## 许可证 + +MIT License - 详见 [LICENSE](LICENSE) diff --git a/skills/stock-master/config.example.json b/skills/stock-master/config.example.json new file mode 100644 index 00000000..fab0163f --- /dev/null +++ b/skills/stock-master/config.example.json @@ -0,0 +1,6 @@ +{ + "_comment": "Stock Master 配置文件示例 - 请复制为 config.json 并填入你的配置", + + "portfolio_path": "./my_portfolio.xlsx", + "feishu_config_path": "./feishu_config.json" +} diff --git a/skills/stock-master/feishu_config.example.json b/skills/stock-master/feishu_config.example.json new file mode 100644 index 00000000..5f2c95ae --- /dev/null +++ b/skills/stock-master/feishu_config.example.json @@ -0,0 +1,16 @@ +{ + "_comment": "飞书多维表格配置文件示例 - 请复制为 feishu_config.json 并填入你的配置", + "_docs": "获取这些信息请参考: https://open.feishu.cn/document/server-docs/docs/bitable-v1/bitable-overview", + + "APP_ID": "your_app_id_here", + "APP_SECRET": "your_app_secret_here", + "APP_TOKEN": "your_bitable_app_token_here", + "TABLE_ID": "your_default_table_id_here", + + "_table_ids": { + "_comment": "可选:配置多个表格 ID", + "signals": "技术信号表的 table_id", + "holdings": "持仓管理表的 table_id", + "trades": "交易记录表的 table_id" + } +} diff --git a/skills/stock-master/references/changelog.md b/skills/stock-master/references/changelog.md new file mode 100644 index 00000000..52dcea84 --- /dev/null +++ b/skills/stock-master/references/changelog.md @@ -0,0 +1,72 @@ +# 更新日志 + +## v3.5.0 (2026-01-22) +- **飞书多维表格集成**: 支持将分析结果同步到飞书 + - 技术信号表: 股票分析结果、评分、买卖建议 + - 持仓管理表: 持仓记录、成本、盈亏 + - 交易记录表: 买卖历史、触发信号 +- **单向同步架构**: 本地分析 → 飞书(飞书为主库,避免冲突) +- **自动表结构初始化**: `feishu_init_tables.py` 一键创建表结构 +- 新增 `scripts/feishu_sync.py` 飞书 API 封装 +- 新增 `scripts/feishu_init_tables.py` 表结构初始化 + +## v3.4.1 (2026-01-22) +- **重命名**: stock-analyzer → stock-master +- **路径变更**: Excel 默认存储路径改为 `/Users/liyanda/Desktop/AI编程/stock master/` + +## v3.4.0 (2026-01-22) +- **K线形态识别**: 识别经典K线形态 + - 单K线: 十字星、锤子线、上吊线、射击之星、倒锤子 + - 双K线: 看涨吞没、看跌吞没 + - 三K线: 早晨之星、黄昏之星、三只白兵、三只乌鸦 +- **趋势形态识别**: 识别经典图表形态 + - 反转形态: 双底(W底)、双顶(M头)、头肩顶、头肩底 + - 整理形态: 上升三角形、下降三角形、对称三角形 +- **形态小白解读**: 用通俗语言解释每种形态的含义和操作建议 +- **评分系统升级**: 形态信号纳入评分,强形态±3分,中等形态±2分,弱形态±1分 +- 新增 `identify_candlestick_patterns()` 函数 +- 新增 `identify_chart_patterns()` 函数 +- 新增 `analyze_patterns()` 综合形态分析函数 +- 新增 `explain_patterns_simple()` 形态解读函数 + +## v3.3.0 (2026-01-22) +- **KDJ 随机指标**: 国内股民最常用的短线指标,支持金叉/死叉/超买超卖判断 +- **MACD/RSI 背离检测**: 自动检测顶背离和底背离,强烈反转信号 +- **增强版支撑阻力位**: 斐波那契回撤位 + 扩展位 + 整数关口 +- **OBV 量能指标**: 能量潮分析,检测资金流向 +- **威廉指标 Williams %R**: 短期超买超卖判断 +- **乖离率 BIAS**: 股价偏离均线程度分析 +- **评分系统优化**: 调整阈值,背离信号权重最高 (±4分) +- **止损止盈优化**: 结合支撑阻力位优化价格建议 +- 新增多个小白解读函数 + +## v3.2.0 (2026-01-21) +- **港股本地计算**: 自动检测港股,使用 Yahoo Finance 本地计算所有指标 +- **优雅降级**: Alpha Vantage API 失败时自动回退到本地计算 +- **ATR 动态止损**: 根据股票波动性自动调整止损距离 +- **成交量分析**: 量价配合分析(放量/缩量 + 涨跌) +- **均线系统**: MA5/10/20/60 + 多空排列判断 +- **仓位建议**: 根据信号强度建议仓位百分比(高信号30%/中20%/低10%) +- **风险收益比**: 自动计算并显示(默认目标 2.5:1) +- 新增 `scripts/indicators.py` 本地指标计算模块 + +## v3.1.0 (2026-01-21) +- 新增「交易建议」工作表自动生成功能 +- 每次持仓分析后自动更新买卖点位到 Excel +- 新增 `update_trading_recommendations()` 函数 +- 优化做T操作填写说明 + +## v3.0.0 (2026-01-21) +- 新增小白友好分析语言 +- 新增买卖点价格建议 +- 新增 Excel 持仓管理 +- 新增持仓分析功能 +- 支持简洁版/详细版报告 + +## v2.0.0 (2026-01-20) +- 混合数据源架构 +- Alpha Vantage MCP 集成 +- 数据源验证 + +## v1.0.0 (2025-10-23) +- 初始版本 diff --git a/skills/stock-master/references/feishu-guide.md b/skills/stock-master/references/feishu-guide.md new file mode 100644 index 00000000..462ac960 --- /dev/null +++ b/skills/stock-master/references/feishu-guide.md @@ -0,0 +1,225 @@ +# 飞书多维表格同步指南 + +Stock Master 支持将分析结果同步到飞书多维表格,实现云端管理。 + +## 功能概览 + +- **技术信号同步**:股票分析结果实时同步 +- **持仓管理**:云端管理持仓数据 +- **交易记录**:记录每次买卖操作 +- **多端访问**:手机、电脑随时查看 + +## 前置准备 + +### 1. 创建飞书应用 + +1. 访问 [飞书开放平台](https://open.feishu.cn/) +2. 创建企业自建应用 +3. 获取 `App ID` 和 `App Secret` + +### 2. 配置应用权限 + +在应用的"权限管理"中添加以下权限: +- `bitable:app` - 多维表格应用权限 +- `bitable:app:readonly` - 读取多维表格 + +### 3. 创建多维表格 + +1. 在飞书中创建新的多维表格 +2. 记录 `App Token`(在表格 URL 中) + +**URL 格式**: +``` +https://xxx.feishu.cn/base/xxxxAPP_TOKENxxxx?table=tblXXXX +``` + +## 配置文件 + +创建 `feishu_config.json`: + +```json +{ + "APP_ID": "cli_xxxxxxxxx", + "APP_SECRET": "xxxxxxxxxxxxxxxx", + "APP_TOKEN": "xxxxxxxxxxxxxxxx", + "TABLE_ID": "tblXXXXXXXXXX" +} +``` + +| 字段 | 说明 | 获取方式 | +|------|------|----------| +| APP_ID | 应用 ID | 开放平台 → 应用凭证 | +| APP_SECRET | 应用密钥 | 开放平台 → 应用凭证 | +| APP_TOKEN | 多维表格 Token | 表格 URL 中获取 | +| TABLE_ID | 默认数据表 ID | 表格 URL 的 table 参数 | + +## 初始化表结构 + +运行初始化脚本创建标准表结构: + +```bash +python scripts/feishu_init_tables.py +``` + +或在 Claude 中说: +``` +初始化飞书表格 +``` + +### 自动创建的表 + +#### 1. 技术信号表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| 股票代码 | 文本 | 股票代码 | +| 股票名称 | 文本 | 公司名称 | +| 当前价格 | 数字 | 最新价格 | +| 综合评分 | 数字 | -10 到 +10 | +| 操作建议 | 单选 | 强烈买入/建议买入/观望/建议卖出/强烈卖出 | +| RSI | 数字 | RSI 指标值 | +| MACD信号 | 单选 | 金叉/多头/空头/死叉 | +| KDJ信号 | 单选 | 金叉/超卖/中性/超买/死叉 | +| 背离信号 | 单选 | 无/底背离/顶背离 | +| 形态信号 | 多选 | 识别到的形态 | +| 止损价 | 数字 | 建议止损价 | +| 止盈价 | 数字 | 建议止盈价 | +| 分析理由 | 文本 | 详细分析理由 | +| 更新时间 | 日期 | 最后更新时间 | + +#### 2. 持仓管理表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| 股票代码 | 文本 | 股票代码 | +| 股票名称 | 文本 | 公司名称 | +| 市场 | 单选 | 美股/港股/A股 | +| 持仓数量 | 数字 | 持有股数 | +| 成本价 | 数字 | 买入均价 | +| 当前价 | 数字 | 最新价格 | +| 市值 | 数字 | 持仓市值 | +| 盈亏金额 | 数字 | 浮动盈亏 | +| 盈亏比例 | 数字 | 盈亏百分比 | +| 买入日期 | 日期 | 建仓日期 | +| 备注 | 文本 | 备注信息 | + +#### 3. 交易记录表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| 股票代码 | 文本 | 股票代码 | +| 交易类型 | 单选 | 买入/卖出/做T买/做T卖 | +| 交易价格 | 数字 | 成交价格 | +| 交易数量 | 数字 | 成交数量 | +| 交易金额 | 数字 | 成交金额 | +| 交易时间 | 日期 | 成交时间 | +| 触发信号 | 文本 | 触发交易的信号 | +| 备注 | 文本 | 交易备注 | + +## 使用命令 + +### 同步分析结果 +``` +分析 AAPL 并同步到飞书 +``` + +### 同步持仓 +``` +同步持仓到飞书 +``` + +### 记录交易 +``` +记录交易:买入 TSLA 10股 @ $400 +``` + +### 查看飞书数据 +``` +查看飞书持仓 +``` + +## 同步逻辑 + +### 数据流向 +``` +本地分析 ────────────→ 飞书多维表格 + (单向同步) +``` + +### 更新规则 +- **技术信号**:按股票代码更新,已存在则覆盖 +- **持仓数据**:按股票代码更新,已存在则覆盖 +- **交易记录**:始终追加,不覆盖 + +## API 使用示例 + +### Python 代码调用 + +```python +from scripts.feishu_sync import FeishuBitable, sync_stock_signal + +# 初始化连接 +bitable = FeishuBitable() + +# 同步信号 +signal_data = { + 'ticker': 'AAPL', + 'name': '苹果', + 'current_price': 185.5, + 'score': 5, + 'action': 'BUY', + 'rsi': 45.2, + 'macd_signal': '金叉', + 'reasons': ['RSI 超卖', 'MACD 金叉'] +} +sync_stock_signal(bitable, signal_data) + +# 测试连接 +from scripts.feishu_sync import test_connection +result = test_connection() +print(result) +``` + +### 快速同步 + +```python +from scripts.feishu_sync import quick_sync_signal + +quick_sync_signal({ + 'ticker': 'TSLA', + 'score': -3, + 'action': 'SELL', + 'rsi': 75, + 'reasons': ['RSI 超买'] +}) +``` + +## 常见问题 + +### Q: 提示 "获取 token 失败" +A: 检查 APP_ID 和 APP_SECRET 是否正确,应用是否已发布。 + +### Q: 提示 "无权限访问" +A: +1. 检查应用权限是否已添加 +2. 检查多维表格是否已授权给应用 +3. 在表格设置中添加应用为协作者 + +### Q: 字段类型不匹配 +A: 运行 `python scripts/feishu_init_tables.py` 重新初始化表结构。 + +### Q: 如何查看 Table ID? +A: 在飞书表格 URL 中: +``` +https://xxx.feishu.cn/base/APP_TOKEN?table=TABLE_ID +``` + +## 安全建议 + +1. **不要提交配置文件**:`feishu_config.json` 已在 `.gitignore` 中 +2. **定期轮换密钥**:建议定期更换 APP_SECRET +3. **最小权限原则**:只授予必要的权限 + +--- + +*如有问题,欢迎在 GitHub 提 Issue!* diff --git a/skills/stock-master/references/investment-wisdom.md b/skills/stock-master/references/investment-wisdom.md new file mode 100644 index 00000000..81f930e6 --- /dev/null +++ b/skills/stock-master/references/investment-wisdom.md @@ -0,0 +1,157 @@ +# 投资智慧与交易哲学 + +> 本文档为投资感悟汇总,可作为分析时的智慧佐证。 + +## 目录 +1. [股民的十大境界](#股民的十大境界) +2. [交易核心原则](#交易核心原则) +3. [技术分析智慧](#技术分析智慧) +4. [心态与纪律](#心态与纪律) +5. [人生哲学](#人生哲学) + +--- + +## 股民的十大境界 + +股民的成长是一个漫长的修炼过程,从新手到高手,需要经历十个境界的蜕变。**能够达到最高境界的股民,万人之中或许才能有一人。** + +### 第一境界:雄心勃勃的新股民 +初入股市的新股民,雄心勃勃,对理财投资充满兴趣,想要在股市里面实现"钱生钱"。这样的股民通常都是在牛市中跑步进场的。 + +### 第二境界:初战告捷的痴迷者 +新股民在牛市中初战告捷,赚了一点小钱,痴迷于每天买入卖出,对于股评家的话深信不疑,每天都会认真地看股评家们煞有其事的分析。 + +### 第三境界:开始怀疑的探索者 +这个时候的新股民,发现股评家的话其实也是不可信的,时而准确时而不准。更重要的是,新股民发现在股市里面赚钱,并非一开始想的那么简单。于是开始找寻各种资料,学着分析公司基本面情况,也就是所谓的价值投资。但很多时候都会发现,公司的财报盈利都很好,但股票不但不涨,反而还下跌!感觉价值投资都是假的。 + +### 第四境界:苦心钻研的韭菜 +这个境界算是从新股民晋升到了韭菜。他们开始苦心钻研各种技术指标,认真学习,按照指标操作,发现时灵时不灵,偶尔能够小赚,但更多的时候还是大亏!操作上也是一团混乱,时而短线,时而长线,对于指标懂的是越来越多,但资金却是越来越少!这个阶段的韭菜,要么就是对指标彻底失望,要么就是对股市开始绝望。 + +### 第五境界:迷茫的老韭菜 ⚠️ 关键分水岭 +这是新韭菜变成老韭菜的阶段。处于这个境界的股民,对于股市也有了属于自己的认识,对于各种技术指标更是能够滔滔不绝讲出来它该怎么使用。但会陷入一个迷茫阶段,因为不论是根据指标操作,还是根据上市公司基本面情况去买入,总是亏损多过于盈利,怎么做怎么错,股市好像和自己在作对一样。 + +有许多人,都会从第一境界慢慢进入到这第五境界中,然后就一直停留在这个阶段。感觉什么技术指标、基本面都是假的,尤其是那些技术指标,假如不理会,随便乱买入的话,成功率居然还会高过依据技术指标买入。然后就认为,什么技术指标基本上通通都没有任何用处,炒股就是赌博!运气好了就赚钱,运气不好就亏钱!然后就放任自己,也不再继续学习了,就凭借直觉买入卖出,渐渐的,又是不知不觉地回到了以前追涨杀跌的日子里。 + +**第五个境界非常关键,许多股民朋友其实都是在这个境界之中,迷失了自己,也停下了脚步。** + +### 第六境界:初成的高手 +能够进入到第六境界的股民,一般就比较少了。第六境界的股民就算是高手了,他们拥有多年的股龄,通过不断的实战,逐渐摸索出来一套属于自己的交易模式。虽然还会亏损,但已经能够控制心态,将亏损视为是交易的一部分,用平常心来对待,交易的时候,盈多亏小。 + +### 第七境界:悟道的交易者 +第七境界的股民,已经不看所谓的任何技术指标,只是简单看看上市公司的基本情况,明白这家公司是做什么的。然后就采取趋势波段交易的方法,当趋势向上,就买入持股不动,当趋势走坏,就直接清仓离场,然后等待下一次交易机会。**这种股民,其实就已经摸索出来属于自己的"道"了。** + +### 第八境界:成熟稳定的交易者 +第八境界的股民,已经有了属于自己的、完整独特的交易模式。他们不论别人怎么说,只相信自己。每天也只是在收盘后,看看买入股票的走势,不破趋势线,对于股价的波动涨跌,毫无任何心理波动。他们的交易模式越发成熟、稳定。 + +### 第九境界:手中有股,心中无股 +第九境界的股民,就像是武侠小说之中所说的"手中有股,心里无股"。他们对于整个股市的波动,都了然于胸,炒股时,进退自如。 + +### 第十境界:财务自由的智者 +第十境界的股民,不会再痴迷于股市,而是将股市当做一个投资场所,每年都可以实现固定盈利。通过这个境界的股民,早就已经达到了财务自由的阶段,对于股市更是十分了解。**他们已经明白,股市其实就是人生的道理!能够达到这个境界的股民,万人之中或许才能够有一人。** + +--- + +## 交易核心原则 + +### 止损的艺术 +- **3%左右就割肉离场**:如果追高买入后第二天跌了,证明看错了,立即止损认错 +- **超出判断范围必须止损**:一旦超出判断的范围,说明后面的走势看不懂了 +- **买入前想好止盈止损**:发现判断错误就必须执行交易纪律,否则就是跳上了失控的列车 + +### 仓位管理 +- **永不满仓**:市场充满未知风险,不论如何看好一只股票,都要学会分批买卖 +- **3136建仓法**:金字塔式买入方法,可以有效拉低持股成本 +- **控制仓位是避免亏损的核心**:将风险控制到自己能够承受的范围里 + +### 交易纪律 +- **上升的股票不做T**:买入后别乱动,想好止盈止损位置,连续大涨就果断卖掉 +- **记录与复盘**:每次买入记录理由和逻辑,亏损离场后对比分析错在哪里 + +--- + +## 技术分析智慧 + +### K线与形态 +- **得双底者得天下**:确定底部要看二次跌到这个价格时收什么样的K线组合 +- **低位十字星**:连续下跌后收低位十字星,可能出现反转,意味着下跌结束 +- **跳空缺口**:出现第一个向上的跳空缺口,是进入强势阶段的显著特点 +- **穿头破脚**:连续上涨后高位放巨量,上破前日最高、跌破前日最低 +- **周K线必看**:不论短线还是长线投资者,周K线都是必须要看的 + +### 成交量与换手率 +- **成交量是汽油**:只有不断放量的股票,才能够走得更远 +- **换手率保持范围**:连续上涨的股票,换手率都会保持在一个范围内 +- **洗盘与出货的区别**:最大的区别就是在于成交量和换手率 + +### 主力资金逻辑 +- **缩量下跌是温水煮青蛙**:主力资金在高位震荡时已派发筹码,缩量下跌迷惑散户 +- **庄股特征明显**:绝对控盘的庄股,庄家可以随心所欲控制在几分钱的波动内 + +--- + +## 心态与纪律 + +### 核心心法 +- **心态第一,技术第二** +- **控制情绪,控制欲望**:想要在市场里成功,这是第一个必须做到的要求 +- **每临大事有静气**:能够克制住自己的脾性,任何事情都能够坦然平静地对待 + +### 牛熊市智慧 +- **牛市重势,熊市重质** +- **牛市炒幻想,熊市炒确定性** +- **牛熊转换之际**:市场主力都会做出种种假象来迷惑你 +- **历史总是惊人的相似,但又不会简单地重复** + +### 复盘的重要性 +- **复盘目的**:对股票池中的股票有了解,分析明天可能的走势 +- **完整复盘至少两三个小时**:不打无准备之仗,不打无把握之仗 +- **市场就是最好的老师**:要学会在市场里面学习 + +--- + +## 人生哲学 + +### 关于财富 +- **财不入急门**:别想着一夜暴富,在股市里一夜暴富是不可能的 +- **道不轻传,法不贱卖,师不顺路,医不叩门** + +### 关于吃苦 +- **穷并不是吃苦,穷就是穷** +- **吃苦的本质**:是一种自控能力、自制能力、坚持能力和深度思考的能力 +- **为了完成某个目标,能够长时间地将精力集中在这个目标上** + +### 关于学习 +- **穷人之所以穷,就是因为他们不愿意去学习** +- **富人或成功的人,共同的特点就是喜欢学习** + +### 关于人性 +- **人的贪婪、恐惧、希望、幻想等等,永远都不会变** +- **太阳底下没有新鲜事** +- **真正的技术派:初级看K线,中级看整体,高级看人性** +- **读懂人性了,也就等于读懂市场了** + +### 技术与心态 +- **技术可以让你赚到钱,但绝对是小钱** +- **想要在市场里赚到大钱,必须有心态的支持** +- **低手追求高技术,高手追求以道驭术** + +--- + +## 风险警示 + +- **炒股千万别融资,别加杠杆**:遇到风险时,连一点抗风险的机会都没有 +- **投资是把钱扔出去,但必须能再弹回来**:这根线就叫做风险控制 +- **凭借运气赚来的钱,凭实力还给了市场** +- **散户的优势是船小好调头**:顺应趋势才是散户要做的事情 + +--- + +## 结语:股市修行之路 + +- **炒股是一条修行的路**:只有不断提高自己,不断回头看看自己的缺点,最终才能够获得成功 +- **看山是山,看山不是山,看山还是山**:炒股同样完全适用 +- **涨多了肯定要跌,跌多了肯定要涨**:就像是潮起潮落一样 + +--- + +*欢迎贡献你的投资智慧!* diff --git a/skills/stock-master/references/mcp-tools.md b/skills/stock-master/references/mcp-tools.md new file mode 100644 index 00000000..0782b2b1 --- /dev/null +++ b/skills/stock-master/references/mcp-tools.md @@ -0,0 +1,93 @@ +# Alpha Vantage MCP 工具调用指南 + +## 目录 +- [获取 RSI](#获取-rsi) +- [获取布林带](#获取布林带) +- [获取报价](#获取报价) +- [响应解析](#响应解析) + +--- + +## 获取 RSI + +```python +mcp__Alpha-Vantage__TOOL_CALL( + tool_name="RSI", + arguments={"symbol": "TSLA", "interval": "daily", + "time_period": 14, "series_type": "close", "datatype": "json"} +) +``` + +**参数说明**: +- `symbol`: 股票代码(仅支持美股) +- `interval`: daily / weekly / monthly +- `time_period`: RSI 周期,通常 14 +- `series_type`: close / open / high / low + +--- + +## 获取布林带 + +```python +mcp__Alpha-Vantage__TOOL_CALL( + tool_name="BBANDS", + arguments={"symbol": "TSLA", "interval": "daily", + "time_period": 20, "series_type": "close", + "nbdevup": 2, "nbdevdn": 2, "datatype": "json"} +) +``` + +**参数说明**: +- `time_period`: 移动平均周期,通常 20 +- `nbdevup`: 上轨标准差倍数,通常 2 +- `nbdevdn`: 下轨标准差倍数,通常 2 + +--- + +## 获取报价 + +```python +mcp__Alpha-Vantage__TOOL_CALL( + tool_name="GLOBAL_QUOTE", + arguments={"symbol": "AAPL", "datatype": "json"} +) +``` + +--- + +## 响应解析 + +### RSI 响应示例 +```json +{ + "Meta Data": { + "Symbol": "TSLA", + "Indicator": "Relative Strength Index (RSI)" + }, + "Technical Analysis: RSI": { + "2026-01-21": {"RSI": "45.2341"} + } +} +``` + +### 布林带响应示例 +```json +{ + "Technical Analysis: BBANDS": { + "2026-01-21": { + "Real Upper Band": "250.5432", + "Real Middle Band": "240.1234", + "Real Lower Band": "229.7036" + } + } +} +``` + +--- + +## 注意事项 + +1. **API 限制**: 免费版每天 25 次请求,注意控制调用频率 +2. **延时**: 免费版数据有 15 分钟延时 +3. **港股不支持**: Alpha Vantage 不支持港股,需使用本地计算 +4. **MACD 不支持**: 免费版不支持 MACD,需本地计算 diff --git a/skills/stock-master/references/portfolio-guide.md b/skills/stock-master/references/portfolio-guide.md new file mode 100644 index 00000000..360e4dd0 --- /dev/null +++ b/skills/stock-master/references/portfolio-guide.md @@ -0,0 +1,152 @@ +# Excel 持仓管理指南 + +Stock Master 提供完整的 Excel 持仓管理功能,帮助你跟踪投资组合。 + +## 快速开始 + +### 创建持仓表格 + +在 Claude 中说: +``` +创建持仓表格 +``` + +系统会自动创建包含以下 Sheet 的 Excel 文件: + +## 表格结构 + +### Sheet 1: 持仓 + +| 列名 | 说明 | 填写方式 | +|------|------|----------| +| 股票代码 | 股票代码 | 手动填写 | +| 股票名称 | 公司名称 | 手动填写 | +| 持仓数量 | 当前持有股数 | 手动填写 | +| 买入均价 | 平均成本价 | 手动填写 | +| 当前价格 | 实时价格 | **自动更新** | +| 市值 | 持仓数量 × 当前价格 | **自动计算** | +| 盈亏金额 | 市值 - 成本 | **自动计算** | +| 盈亏比例 | 盈亏百分比 | **自动计算** | +| 买入日期 | 建仓日期 | 手动填写 | +| 备注 | 备注信息 | 手动填写 | + +**股票代码格式**: +- 美股:`AAPL`, `TSLA`, `GOOGL` +- 港股:`0700.HK`, `9988.HK` +- A股:`600519.SS`(上海), `000001.SZ`(深圳) + +### Sheet 2: 交易记录 + +| 列名 | 说明 | +|------|------| +| 日期 | 交易日期 | +| 股票代码 | 交易的股票 | +| 操作 | 买入 / 卖出 | +| 数量 | 交易股数 | +| 价格 | 成交价格 | +| 金额 | 交易总额 | +| 手续费 | 交易费用 | +| 备注 | 交易备注 | + +### Sheet 3: 账户汇总 + +自动汇总的账户信息: +- 总投入 +- 当前市值 +- 总盈亏 +- 收益率 +- 最后更新时间 + +### Sheet 4: 使用说明 + +内置的使用指南。 + +## 使用命令 + +### 分析持仓 +``` +分析我的持仓 +``` +Claude 会: +1. 读取你的持仓数据 +2. 获取每只股票的实时价格 +3. 更新 Excel 中的价格和盈亏 +4. 给出每只股票的技术分析建议 + +### 更新价格 +``` +更新持仓价格 +``` +只更新价格,不做分析。 + +### 持仓建议 +``` +我的持仓哪些该卖了? +``` +根据技术指标给出持仓调整建议。 + +## 做T操作说明 + +持仓表只记录"结果",不需要记录每次做T操作。 + +**示例**: +- TSLA 做了几轮T,最终持仓 100股,均价 $380 +- 直接填:数量=100,均价=380 +- 均价可从券商 App 查看 + +如需追踪每次操作,可在"交易记录"表记录。 + +## 配置文件 + +### 默认路径 + +系统按以下顺序查找持仓文件: +1. 环境变量 `PORTFOLIO_PATH` +2. `config.json` 中的 `portfolio_path` +3. 当前目录的 `my_portfolio.xlsx` + +### 自定义路径 + +创建 `config.json`: +```json +{ + "portfolio_path": "/your/custom/path/portfolio.xlsx" +} +``` + +或设置环境变量: +```bash +export PORTFOLIO_PATH="/your/custom/path/portfolio.xlsx" +``` + +## 示例模板 + +创建后的持仓表包含示例数据,帮助理解格式: + +``` +| 股票代码 | 股票名称 | 持仓数量 | 买入均价 | 当前价格 | 市值 | 盈亏金额 | 盈亏比例 | 买入日期 | 备注 | +|----------|----------|----------|----------|----------|------|----------|----------|----------|------| +| AAPL | 苹果公司 | 10 | 180.00 | | | | | 2024-01-15 | 长期持有 | +| TSLA | 特斯拉 | 5 | 400.00 | | | | | 2024-06-01 | | +| GOOGL | 谷歌 | 3 | 150.00 | | | | | 2024-03-20 | | +``` + +> 提示:删除示例数据,填入你自己的持仓即可。 + +## 常见问题 + +### Q: 如何处理分红? +A: 分红后股价会除权,建议在备注中记录分红信息。 + +### Q: 多个账户怎么管理? +A: 可以创建多个 Excel 文件,分析时指定路径: +``` +分析 /path/to/账户1.xlsx 的持仓 +``` + +### Q: 港股如何处理汇率? +A: 目前按港币计算,建议在备注中记录汇率。 + +--- + +*如有问题,欢迎在 GitHub 提 Issue!* diff --git a/skills/stock-master/references/scripts-guide.md b/skills/stock-master/references/scripts-guide.md new file mode 100644 index 00000000..199c3a05 --- /dev/null +++ b/skills/stock-master/references/scripts-guide.md @@ -0,0 +1,225 @@ +# 脚本详细说明 v3.4 + +## 目录 +- [main.py - 主协调器](#mainpy---主协调器) +- [beginner_analyzer.py - 小白分析模块](#beginner_analyzerpy---小白分析模块) +- [indicators.py - 本地指标计算](#indicatorspy---本地指标计算) +- [portfolio.py - 持仓管理](#portfoliopy---持仓管理) + +--- + +## main.py - 主协调器 + +混合数据源架构的核心协调器。 + +### 数据源策略 +| 数据 | 来源 | 原因 | +|------|------|------| +| 实时价格 | Yahoo Finance | 接近实时,无请求限制 | +| MACD | Yahoo + 本地计算 | AV 免费版不支持 | +| RSI | Alpha Vantage MCP | 官方预计算,准确 | +| 布林带 | Alpha Vantage MCP | 官方预计算,准确 | +| KDJ/OBV/背离/形态 | 本地计算 | AV 不提供 | + +--- + +## beginner_analyzer.py - 小白分析模块 + +用通俗语言解释技术指标。 + +### 小白解读函数 +| 函数 | 功能 | +|------|------| +| `explain_rsi_simple()` | RSI 通俗解释 | +| `explain_macd_simple()` | MACD 通俗解释 | +| `explain_bollinger_simple()` | 布林带通俗解释 | +| `explain_volume_simple()` | 成交量通俗解释 | +| `explain_ma_simple()` | 均线通俗解释 | +| `explain_atr_simple()` | ATR 波动性解释 | +| `explain_kdj_simple()` | KDJ 通俗解释 | +| `explain_divergence_simple()` | 背离信号解释 | +| `explain_obv_simple()` | OBV 量能解释 | +| `explain_williams_simple()` | 威廉指标解释 | +| `explain_bias_simple()` | 乖离率解释 | +| `explain_support_resistance_simple()` | 支撑阻力位解释 | +| `explain_candlestick_pattern_simple()` | [v3.4] K线形态解释 | +| `explain_chart_pattern_simple()` | [v3.4] 趋势形态解释 | +| `explain_patterns_simple()` | [v3.4] 综合形态解释 | + +### 报告生成 +| 函数 | 功能 | +|------|------| +| `generate_trading_recommendation()` | 综合交易建议 (含评分) | +| `format_simple_report()` | 简洁版报告 | +| `format_detailed_report()` | 详细版报告 | + +### 评分规则 (v3.4) +| 分数范围 | 建议 | +|----------|------| +| ≥6 | 强烈买入 | +| 3-5 | 建议买入 | +| -2~2 | 观望 | +| -3~-5 | 建议卖出 | +| ≤-6 | 强烈卖出 | + +### 评分权重 +| 信号类型 | 权重 | 说明 | +|----------|------|------| +| 背离信号 | ±4 | 最强信号 | +| 强形态信号 | ±3 | [v3.4] 三只白兵/乌鸦、早晨/黄昏之星、头肩、双底双顶 | +| 金叉/死叉 | ±3 | 强信号 | +| 中等形态 | ±2 | [v3.4] 吞没、三角形 | +| 超买/超卖 | ±2~3 | 中强信号 | +| 弱形态/趋势确认 | ±1~2 | 辅助信号 | + +--- + +## indicators.py - 本地指标计算 + +支持港股和 API 降级时的本地计算。 + +### 基础指标函数 +| 函数 | 功能 | +|------|------| +| `calculate_rsi()` | 计算 RSI | +| `calculate_bollinger_bands()` | 计算布林带 | +| `calculate_macd()` | 计算 MACD | +| `calculate_atr()` | 计算 ATR | +| `calculate_ma_system()` | 计算均线系统 | +| `calculate_volume_analysis()` | 成交量分析 | + +### v3.3 新增指标函数 +| 函数 | 功能 | +|------|------| +| `calculate_kdj()` | KDJ 随机指标 | +| `calculate_obv()` | OBV 能量潮 | +| `calculate_williams_r()` | 威廉指标 | +| `calculate_bias()` | 乖离率 | +| `detect_divergence()` | 通用背离检测 | +| `detect_macd_divergence()` | MACD 背离检测 | +| `detect_rsi_divergence()` | RSI 背离检测 | +| `calculate_support_resistance_enhanced()` | 增强版支撑阻力位 | + +### v3.4 形态识别函数 +| 函数 | 功能 | +|------|------| +| `identify_candlestick_patterns()` | K线形态识别 | +| `identify_chart_patterns()` | 趋势形态识别 | +| `analyze_patterns()` | 综合形态分析 | + +### K线形态识别 +| 形态 | 信号 | 强度 | +|------|------|------| +| 三只白兵 | 看涨 | 非常强 | +| 三只乌鸦 | 看跌 | 非常强 | +| 早晨之星 | 看涨 | 强 | +| 黄昏之星 | 看跌 | 强 | +| 看涨吞没 | 看涨 | 强 | +| 看跌吞没 | 看跌 | 强 | +| 锤子线 | 看涨 | 中 | +| 上吊线 | 看跌 | 中 | +| 十字星 | 中性 | 弱 | + +### 趋势形态识别 +| 形态 | 信号 | 说明 | +|------|------|------| +| 双底(W底) | 看涨 | 经典底部反转 | +| 双顶(M头) | 看跌 | 经典顶部反转 | +| 头肩底 | 看涨 | 可靠底部反转 | +| 头肩顶 | 看跌 | 可靠顶部反转 | +| 上升三角形 | 看涨 | 通常向上突破 | +| 下降三角形 | 看跌 | 通常向下突破 | +| 对称三角形 | 中性 | 等待突破方向 | + +### 支撑阻力位计算方法 +1. **近期高低点** - 60日内最高最低 +2. **斐波那契回撤** - 23.6%, 38.2%, 50%, 61.8%, 78.6% +3. **斐波那契扩展** - 127.2%, 161.8%, 200% +4. **整数关口** - 心理价位 + +### 综合分析 +```python +analyze_stock_local(ticker, period='3mo') +``` +返回所有指标的完整分析结果,包括: +- 基础指标 (RSI/MACD/布林带/ATR/均线/成交量) +- v3.3 新增 (KDJ/OBV/威廉/乖离率) +- 支撑阻力位 (斐波那契) +- 背离检测 (MACD/RSI) +- v3.4 形态识别 (K线形态/趋势形态) +- 动态止损建议 + +--- + +## portfolio.py - 持仓管理 + +Excel 持仓表管理。 + +### 核心函数 +| 函数 | 功能 | +|------|------| +| `create_portfolio_template()` | 创建 Excel 模板 | +| `read_portfolio()` | 读取持仓数据 | +| `update_portfolio_prices()` | 更新实时价格 | +| `update_trading_recommendations()` | 更新交易建议表 | +| `format_portfolio_summary()` | 格式化持仓汇总 | + +### 股票代码格式 +- 美股: `AAPL`, `TSLA`, `GOOGL` +- 港股: `0700.HK`, `9988.HK` +- A股: `600519.SS` (上海), `000001.SZ` (深圳) + +--- + +## feishu_sync.py - 飞书多维表格同步 [v3.5] + +将分析结果同步到飞书多维表格。 + +### 核心类 +| 类 | 功能 | +|------|------| +| `FeishuBitable` | 飞书多维表格 API 封装 | + +### 同步函数 +| 函数 | 功能 | +|------|------| +| `sync_stock_signal()` | 同步股票技术信号(覆盖更新) | +| `sync_holding()` | 同步持仓数据(覆盖更新) | +| `sync_trade_record()` | 同步交易记录(追加模式) | +| `batch_sync_signals()` | 批量同步多只股票 | +| `quick_sync_signal()` | 快速同步(一行调用) | +| `test_connection()` | 测试飞书连接 | + +### 飞书表结构 +| 表名 | Table ID | 用途 | +|------|----------|------| +| 数据表 | tbldtKCoANcpvitS | 技术信号 | +| 持仓管理 | tblHkx30pGT1QzOq | 持仓记录 | +| 交易记录 | tblTnlPDFmr5gmSV | 交易历史 | + +### 使用示例 +```python +from feishu_sync import FeishuBitable, sync_stock_signal + +bitable = FeishuBitable() +sync_stock_signal(bitable, { + 'ticker': 'AAPL', + 'score': 5, + 'action': '建议买入', + ... +}) +``` + +--- + +## feishu_init_tables.py - 表结构初始化 + +一键初始化飞书多维表格结构。 + +### 函数 +| 函数 | 功能 | +|------|------| +| `init_signal_table()` | 初始化技术信号表字段 | +| `create_holdings_table()` | 创建持仓管理表 | +| `create_trades_table()` | 创建交易记录表 | +| `init_all_tables()` | 一键初始化所有表 | diff --git a/skills/stock-master/requirements.txt b/skills/stock-master/requirements.txt new file mode 100644 index 00000000..e23b6755 --- /dev/null +++ b/skills/stock-master/requirements.txt @@ -0,0 +1,24 @@ +# 股票分析器 Skill v3.0 - 依赖项 + +# ============================================ +# 数据获取 - Yahoo Finance (实时价格 + MACD) +# ============================================ +yfinance>=0.2.0 + +# ============================================ +# 数据处理 +# ============================================ +pandas>=2.0.0 +numpy>=1.24.0 + +# ============================================ +# Excel 持仓管理 +# ============================================ +openpyxl>=3.1.0 + +# ============================================ +# 注意事项 +# ============================================ +# 1. Alpha Vantage 数据通过 MCP 工具获取,无需额外安装 +# 2. yfinance 用于实时价格和 MACD 计算 +# 3. openpyxl 用于读写 Excel 持仓表格 diff --git a/skills/stock-master/scripts/beginner_analyzer.py b/skills/stock-master/scripts/beginner_analyzer.py new file mode 100644 index 00000000..9d573a46 --- /dev/null +++ b/skills/stock-master/scripts/beginner_analyzer.py @@ -0,0 +1,925 @@ +""" +小白友好股票分析模块 v3.4 + +功能: +- 用通俗易懂的语言解释技术指标 +- 综合分析给出买卖点建议 +- 支持简洁版和详细版报告 +- 港股本地计算支持 +- ATR 动态止损 +- 成交量/均线分析 +- 仓位建议 + +v3.4 新增: +- K线形态识别小白解读(锤子线、吞没、十字星、早晨之星等) +- 趋势形态识别小白解读(双底、双顶、头肩顶/底、三角形) +- 形态信号纳入评分系统 + +v3.3: +- KDJ 随机指标小白解读 +- 背离信号解读 +- 支撑阻力位解读 +- OBV 量能解读 +- 威廉指标/乖离率解读 +""" + +from typing import Dict, List, Optional, Any, Tuple +from datetime import datetime, timedelta +from dataclasses import dataclass, field + + +@dataclass +class TradingSignal: + """交易信号""" + action: str # BUY, SELL, HOLD + confidence: str # 高, 中, 低 + buy_price: Optional[float] = None # 建议买入价 + sell_price: Optional[float] = None # 建议卖出价 + stop_loss: Optional[float] = None # 止损价 + take_profit: Optional[float] = None # 止盈价 + reasons: List[str] = field(default_factory=list) # 理由列表 + # v3.2 字段 + atr: Optional[float] = None # ATR 值 + atr_percent: Optional[float] = None # ATR 百分比 + risk_reward_ratio: Optional[float] = None # 风险收益比 + suggested_position: Optional[float] = None # 建议仓位百分比 + volume_signal: Optional[str] = None # 成交量信号 + ma_trend: Optional[str] = None # 均线趋势 + # v3.3 新增字段 + kdj_signal: Optional[str] = None # KDJ 信号 + divergence_signal: Optional[str] = None # 背离信号 + obv_signal: Optional[str] = None # OBV 信号 + support_price: Optional[float] = None # 最近支撑位 + resistance_price: Optional[float] = None # 最近阻力位 + score: Optional[int] = None # 综合评分 + # v3.4 形态识别 + patterns_signal: Optional[str] = None # 形态综合信号 + patterns_count: Optional[int] = None # 识别到的形态数量 + + +def explain_rsi_simple(rsi: float) -> str: + """用小白语言解释 RSI""" + if rsi < 30: + return f"🟢 **超卖** ({rsi:.1f}) - 股票被卖得太多了,就像商场大甩卖,价格可能已经跌过头,是潜在的捡便宜机会" + elif rsi < 40: + return f"🟡 **偏弱** ({rsi:.1f}) - 股票有点疲软,买家不太积极,但还没到跌过头的程度" + elif rsi < 60: + return f"⚪ **中性** ({rsi:.1f}) - 买卖力量均衡,股票在正常波动,没有明显的超买或超卖" + elif rsi < 70: + return f"🟡 **偏强** ({rsi:.1f}) - 买家比较积极,股票走势还不错,但要注意别追高" + else: + return f"🔴 **超买** ({rsi:.1f}) - 股票被买得太多了,就像热门商品被抢购一空,价格可能涨过头,要小心回调" + + +def explain_macd_simple(macd_line: float, signal_line: float, histogram: float, prev_histogram: float = None) -> str: + """用小白语言解释 MACD""" + # 判断趋势和交叉 + if prev_histogram is not None: + if histogram > 0 and prev_histogram <= 0: + return f"🟢 **金叉出现** - 短期上涨动能超过了长期动能,就像汽车踩了油门开始加速,是买入信号" + elif histogram < 0 and prev_histogram >= 0: + return f"🔴 **死叉出现** - 短期动能开始减弱,就像汽车松了油门开始减速,是卖出警告" + + if histogram > 0: + strength = "强劲" if histogram > 1 else "温和" + return f"🟢 **多头趋势** - 上涨动能{strength},就像顺风骑车,省力又快" + else: + strength = "明显" if histogram < -1 else "轻微" + return f"🔴 **空头趋势** - 下跌动能{strength},就像逆风骑车,需要更多力气" + + +def explain_bollinger_simple(price: float, upper: float, middle: float, lower: float) -> str: + """用小白语言解释布林带""" + band_width = upper - lower + position = (price - lower) / band_width * 100 if band_width > 0 else 50 + + if price < lower: + return f"🟢 **跌破下轨** - 股价已经跌到了'地板'下面 (${lower:.2f}),就像橡皮筋拉得太长,可能要弹回来了" + elif price < lower + band_width * 0.2: + return f"🟢 **接近下轨** - 股价在'地板'附近 (${lower:.2f}),处于相对低位,可能是买入机会" + elif price > upper: + return f"🔴 **突破上轨** - 股价已经涨到了'天花板'上面 (${upper:.2f}),涨得有点猛,可能要回落" + elif price > upper - band_width * 0.2: + return f"🟡 **接近上轨** - 股价在'天花板'附近 (${upper:.2f}),处于相对高位,要注意回调风险" + else: + return f"⚪ **正常区间** - 股价在中间位置 (中轨 ${middle:.2f}),波动正常" + + +def explain_volume_simple(volume_ratio: float, pattern: str) -> str: + """用小白语言解释成交量""" + if pattern == "放量上涨": + return f"📈 **放量上涨** (量比 {volume_ratio:.1f}) - 买盘积极涌入,像超市促销引来大批顾客,上涨动力充足" + elif pattern == "放量下跌": + return f"📉 **放量下跌** (量比 {volume_ratio:.1f}) - 卖盘大量涌出,像恐慌性抛售,需要警惕" + elif pattern == "缩量上涨": + return f"📈 **缩量上涨** (量比 {volume_ratio:.1f}) - 涨是涨了但买家不多,像没人气的促销,后劲可能不足" + elif pattern == "缩量下跌": + return f"📉 **缩量下跌** (量比 {volume_ratio:.1f}) - 跌但卖家也不多了,像甩卖接近尾声,可能快到底了" + elif pattern == "放量震荡": + return f"⚡ **放量震荡** (量比 {volume_ratio:.1f}) - 交易活跃但方向不明,多空在激烈博弈" + else: + return f"➡️ **量价平稳** (量比 {volume_ratio:.1f}) - 一切正常,没有异常信号" + + +def explain_ma_simple(arrangement: str, price_above: List[str], price_below: List[str]) -> str: + """用小白语言解释均线""" + if arrangement == "多头排列": + return f"🟢 **多头排列** - 短期均线在上,长期均线在下,像排队的人越排越高,趋势向上" + elif arrangement == "空头排列": + return f"🔴 **空头排列** - 短期均线在下,长期均线在上,像滑梯往下滑,趋势向下" + else: + above_str = '/'.join(price_above) if price_above else "无" + below_str = '/'.join(price_below) if price_below else "无" + return f"🟡 **均线缠绕** - 均线交织在一起,方向不明朗(价格在 {above_str} 上方,在 {below_str} 下方)" + + +def explain_atr_simple(atr_percent: float) -> str: + """用小白语言解释 ATR(波动性)""" + if atr_percent > 5: + return f"⚠️ **高波动** ({atr_percent:.1f}%) - 股价波动剧烈,像坐过山车,风险较大但机会也大" + elif atr_percent > 3: + return f"🔔 **中等波动** ({atr_percent:.1f}%) - 股价有一定波动,正常范围" + else: + return f"😌 **低波动** ({atr_percent:.1f}%) - 股价比较稳定,适合稳健型投资" + + +# ============================================ +# v3.3 新增小白解读函数 +# ============================================ + +def explain_kdj_simple(k: float, d: float, j: float, signal: str) -> str: + """用小白语言解释 KDJ 随机指标""" + if signal == 'golden_cross': + return f"🟢 **KDJ 金叉** (K={k:.0f}, D={d:.0f}, J={j:.0f}) - 短期买入信号,像绿灯亮了可以出发" + elif signal == 'death_cross': + return f"🔴 **KDJ 死叉** (K={k:.0f}, D={d:.0f}, J={j:.0f}) - 短期卖出信号,像红灯亮了要刹车" + elif signal == 'overbought' or signal == 'high_zone': + return f"🔴 **KDJ 超买** (K={k:.0f}, D={d:.0f}, J={j:.0f}) - 短期涨太快了,像弹簧压得太紧可能要回弹" + elif signal == 'oversold' or signal == 'low_zone': + return f"🟢 **KDJ 超卖** (K={k:.0f}, D={d:.0f}, J={j:.0f}) - 短期跌太多了,像皮球落地可能要反弹" + else: + return f"⚪ **KDJ 中性** (K={k:.0f}, D={d:.0f}, J={j:.0f}) - 目前没有明显的超买超卖信号" + + +def explain_divergence_simple(divergence_type: str, indicator_name: str = "MACD") -> str: + """用小白语言解释背离信号""" + if divergence_type == 'bullish': + return f"🟢 **{indicator_name} 底背离** - 价格在创新低,但{indicator_name}没有创新低,说明下跌动能在减弱,像马拉松跑到后面速度慢下来了,可能要反弹" + elif divergence_type == 'bearish': + return f"🔴 **{indicator_name} 顶背离** - 价格在创新高,但{indicator_name}没有创新高,说明上涨动能在减弱,像爬山快到顶了越来越吃力,小心回调" + else: + return f"⚪ **无{indicator_name}背离** - 价格和指标走势一致,趋势正常" + + +def explain_support_resistance_simple( + current_price: float, + nearest_support: dict, + nearest_resistance: dict +) -> str: + """用小白语言解释支撑阻力位""" + parts = [] + + if nearest_support: + support_price = nearest_support['price'] + distance_pct = (current_price - support_price) / current_price * 100 + parts.append(f"📉 **最近支撑位**: ${support_price:.2f} (距离 {distance_pct:.1f}%) - 跌到这里可能会有买盘接住,像地板一样") + + if nearest_resistance: + resist_price = nearest_resistance['price'] + distance_pct = (resist_price - current_price) / current_price * 100 + parts.append(f"📈 **最近阻力位**: ${resist_price:.2f} (距离 {distance_pct:.1f}%) - 涨到这里可能会有卖盘压制,像天花板一样") + + if not parts: + return "暂无明确的支撑阻力位" + + return "\n".join(parts) + + +def explain_obv_simple(signal: str, obv_trend: str, price_trend: str) -> str: + """用小白语言解释 OBV 能量潮""" + if signal == 'confirmed_up': + return "🟢 **OBV 确认上涨** - 价格涨,资金也在流入,像涨潮一样水涨船高,趋势健康" + elif signal == 'confirmed_down': + return "🔴 **OBV 确认下跌** - 价格跌,资金也在流出,像退潮一样水落船低,趋势延续" + elif signal == 'bullish_divergence': + return "🟢 **OBV 底背离** - 价格在跌,但资金在悄悄流入,像有人在偷偷抄底,关注反弹机会" + elif signal == 'bearish_divergence': + return "🔴 **OBV 顶背离** - 价格在涨,但资金在悄悄流出,像有人在偷偷出货,警惕回调风险" + else: + return "⚪ **OBV 中性** - 量价关系正常" + + +def explain_williams_simple(wr: float, signal: str) -> str: + """用小白语言解释威廉指标""" + if signal == 'overbought': + return f"🔴 **威廉指标超买** ({wr:.0f}) - 短期涨得太急,像冲刺跑太快要喘气,可能要回调" + elif signal == 'oversold': + return f"🟢 **威廉指标超卖** ({wr:.0f}) - 短期跌得太急,像跌倒了要爬起来,可能要反弹" + else: + return f"⚪ **威廉指标中性** ({wr:.0f}) - 目前处于正常区间" + + +def explain_bias_simple(bias6: float, signal: str) -> str: + """用小白语言解释乖离率""" + if signal == 'overbought': + return f"🔴 **乖离率偏高** ({bias6:.1f}%) - 股价跑得比均线快太多了,像跑步冲太快会累,可能要回来休息(回调)" + elif signal == 'oversold': + return f"🟢 **乖离率偏低** ({bias6:.1f}%) - 股价跌得比均线远太多了,像橡皮筋拉太长会弹回来,可能要反弹" + else: + return f"⚪ **乖离率正常** ({bias6:.1f}%) - 股价和均线走得差不多齐,比较健康" + + +# ============================================ +# v3.4 形态识别小白解读 +# ============================================ + +# K线形态中英文映射 +CANDLESTICK_NAMES = { + 'doji': '十字星', + 'hammer': '锤子线', + 'hanging_man': '上吊线', + 'bullish_engulfing': '看涨吞没', + 'bearish_engulfing': '看跌吞没', + 'morning_star': '早晨之星', + 'evening_star': '黄昏之星', + 'three_white_soldiers': '三只白兵', + 'three_black_crows': '三只乌鸦', + 'shooting_star': '射击之星', + 'inverted_hammer': '倒锤子' +} + +# 趋势形态中英文映射 +CHART_PATTERN_NAMES = { + 'double_bottom': '双底', + 'double_top': '双顶', + 'head_and_shoulders_top': '头肩顶', + 'head_and_shoulders_bottom': '头肩底', + 'ascending_triangle': '上升三角形', + 'descending_triangle': '下降三角形', + 'symmetric_triangle': '对称三角形' +} + + +def explain_candlestick_pattern_simple(pattern: dict) -> str: + """用小白语言解释K线形态""" + pattern_type = pattern.get('pattern', '') + signal = pattern.get('signal', 'neutral') + strength = pattern.get('strength', 'medium') + + cn_name = CANDLESTICK_NAMES.get(pattern_type, pattern_type) + + # 强度描述 + strength_desc = { + 'very_strong': '非常强烈', + 'strong': '强烈', + 'medium': '中等', + 'weak': '较弱' + }.get(strength, '中等') + + # 各形态的小白解读 + explanations = { + 'doji': "⚪ **十字星** - 多空双方势均力敌,就像拔河比赛打成平手。当前趋势可能要变化,要密切观察", + 'hammer': "🟢 **锤子线** (看涨) - 像一把锤子倒立,下影线很长说明下方有人接盘。出现在下跌后,是见底信号", + 'hanging_man': "🔴 **上吊线** (看跌) - 形状像锤子但出现在上涨后,说明上方卖压开始出现。可能是见顶信号", + 'bullish_engulfing': "🟢 **看涨吞没** (强信号) - 大阳线完全包住前一根阴线,像大鱼吃小鱼。买方力量占优,反转信号", + 'bearish_engulfing': "🔴 **看跌吞没** (强信号) - 大阴线完全包住前一根阳线,卖方力量压倒买方。下跌信号", + 'morning_star': "🟢 **早晨之星** (强信号) - 三根K线组成,像黎明前的启明星。典型的底部反转形态,买入机会", + 'evening_star': "🔴 **黄昏之星** (强信号) - 三根K线组成,像日落前的昏星。典型的顶部反转形态,卖出信号", + 'three_white_soldiers': "🟢 **三只白兵** (非常强) - 连续三根阳线稳步上涨,像士兵列队前进。强烈的上涨信号", + 'three_black_crows': "🔴 **三只乌鸦** (非常强) - 连续三根阴线稳步下跌,像乌鸦报丧。强烈的下跌信号", + 'shooting_star': "🔴 **射击之星** - 上影线很长,像流星划过。出现在上涨后说明上方抛压重,可能见顶", + 'inverted_hammer': "🟢 **倒锤子** - 出现在下跌后的长上影线,买方尝试反攻。如果次日确认,是反转信号" + } + + explanation = explanations.get(pattern_type, f"识别到 {cn_name} 形态") + return f"{explanation}\n ⭐ 信号强度: {strength_desc}" + + +def explain_chart_pattern_simple(pattern: dict) -> str: + """用小白语言解释趋势形态""" + pattern_type = pattern.get('pattern', '') + signal = pattern.get('signal', 'neutral') + strength = pattern.get('strength', 'medium') + + cn_name = CHART_PATTERN_NAMES.get(pattern_type, pattern_type) + + # 强度描述 + strength_desc = { + 'very_strong': '非常可靠', + 'strong': '比较可靠', + 'medium': '参考意义', + 'weak': '需要验证' + }.get(strength, '参考意义') + + # 各形态的小白解读 + explanations = { + 'double_bottom': "🟢 **双底形态** (W底) - 股价两次探底后反弹,像字母W。经典的底部反转形态,突破颈线后看涨", + 'double_top': "🔴 **双顶形态** (M头) - 股价两次冲高后回落,像字母M。经典的顶部形态,跌破颈线后看跌", + 'head_and_shoulders_top': "🔴 **头肩顶** - 中间高两边低,像人的头和肩膀。是最可靠的顶部反转形态之一", + 'head_and_shoulders_bottom': "🟢 **头肩底** - 中间低两边高,倒过来的头肩形态。是可靠的底部反转信号", + 'ascending_triangle': "🟢 **上升三角形** - 底部逐步抬高,顶部水平。说明买方逐渐占优,通常向上突破", + 'descending_triangle': "🔴 **下降三角形** - 顶部逐步降低,底部水平。说明卖方逐渐占优,通常向下突破", + 'symmetric_triangle': "⚪ **对称三角形** - 高点降低、低点抬高,形成收敛。突破方向不确定,等待突破后跟进" + } + + explanation = explanations.get(pattern_type, f"识别到 {cn_name} 形态") + return f"{explanation}\n ⭐ 可靠度: {strength_desc}" + + +def explain_patterns_simple(patterns_data: dict) -> str: + """综合解释所有识别到的形态""" + if not patterns_data: + return "⚪ 暂未识别到明显形态" + + all_patterns = patterns_data.get('all_patterns', []) + if not all_patterns: + return "⚪ 暂未识别到明显形态" + + result_lines = ["📊 **形态识别结果**"] + + # K线形态 + candlestick = patterns_data.get('candlestick_patterns', []) + if candlestick: + result_lines.append("\n**K线形态:**") + for p in candlestick[:3]: # 最多显示3个 + result_lines.append(f" • {explain_candlestick_pattern_simple(p)}") + + # 趋势形态 + chart = patterns_data.get('chart_patterns', []) + if chart: + result_lines.append("\n**趋势形态:**") + for p in chart[:2]: # 最多显示2个 + result_lines.append(f" • {explain_chart_pattern_simple(p)}") + + # 综合判断 + signal = patterns_data.get('signal', 'neutral') + bullish = patterns_data.get('bullish_count', 0) + bearish = patterns_data.get('bearish_count', 0) + + result_lines.append(f"\n**形态综合判断:**") + if signal == 'bullish': + result_lines.append(f" 🟢 看涨形态占优 (看涨{bullish}个 vs 看跌{bearish}个)") + elif signal == 'bearish': + result_lines.append(f" 🔴 看跌形态占优 (看跌{bearish}个 vs 看涨{bullish}个)") + else: + result_lines.append(f" ⚪ 形态信号中性 (看涨{bullish}个 vs 看跌{bearish}个)") + + return "\n".join(result_lines) + + +def explain_trend_simple(prices: List[float], period_name: str = "近期") -> str: + """用小白语言解释趋势""" + if len(prices) < 5: + return "数据不足,无法判断趋势" + + start_price = prices[0] + end_price = prices[-1] + change_pct = (end_price - start_price) / start_price * 100 + + # 计算波动性 + max_price = max(prices) + min_price = min(prices) + volatility = (max_price - min_price) / start_price * 100 + + if change_pct > 10: + trend = f"📈 **强势上涨** - {period_name}涨了 {change_pct:.1f}%,像坐电梯往上,势头很猛" + elif change_pct > 3: + trend = f"📈 **温和上涨** - {period_name}涨了 {change_pct:.1f}%,像爬楼梯,稳步向上" + elif change_pct > -3: + trend = f"➡️ **横盘震荡** - {period_name}基本持平 ({change_pct:+.1f}%),在一个区间内来回波动" + elif change_pct > -10: + trend = f"📉 **温和下跌** - {period_name}跌了 {abs(change_pct):.1f}%,像下楼梯,逐步下行" + else: + trend = f"📉 **大幅下跌** - {period_name}跌了 {abs(change_pct):.1f}%,跌势比较急" + + # 添加波动性说明 + if volatility > 20: + trend += f"\n ⚠️ 波动较大 ({volatility:.1f}%),坐过山车的感觉,风险较高" + elif volatility > 10: + trend += f"\n 🔔 波动中等 ({volatility:.1f}%),有起有伏但还算正常" + + return trend + + +def calculate_support_resistance(prices: List[float], current_price: float) -> Dict[str, float]: + """计算支撑位和阻力位""" + if len(prices) < 20: + return {} + + # 简单方法:使用近期高低点 + recent_high = max(prices[-20:]) + recent_low = min(prices[-20:]) + + # 计算关键位置 + result = { + 'strong_resistance': recent_high, # 强阻力位 = 近期最高 + 'weak_resistance': current_price + (recent_high - current_price) * 0.382, # 斐波那契 + 'weak_support': current_price - (current_price - recent_low) * 0.382, + 'strong_support': recent_low, # 强支撑位 = 近期最低 + } + + return result + + +def generate_trading_recommendation( + ticker: str, + current_price: float, + rsi: float, + macd_histogram: float, + prev_macd_histogram: float, + bb_upper: float, + bb_middle: float, + bb_lower: float, + prices_1m: List[float] = None, + prices_3m: List[float] = None, + # v3.2 参数 + atr: float = None, + atr_percent: float = None, + volume_ratio: float = None, + volume_signal: str = None, + ma_trend: str = None, + ma_arrangement: str = None, + # v3.3 新增参数 + kdj_k: float = None, + kdj_d: float = None, + kdj_j: float = None, + kdj_signal: str = None, + macd_divergence: str = None, + rsi_divergence: str = None, + obv_signal: str = None, + williams_signal: str = None, + bias_signal: str = None, + nearest_support: float = None, + nearest_resistance: float = None, + # v3.4 形态识别参数 + patterns_signal: str = None, # 'bullish', 'bearish', 'neutral' + patterns_data: dict = None # 完整形态数据 +) -> TradingSignal: + """ + 综合分析生成交易建议 (v3.4) + + 综合策略: + 1. RSI 超卖/超买 + 2. MACD 金叉/死叉 + 3. 布林带位置 + 4. 趋势判断 + 5. 成交量配合 + 6. 均线排列 + 7. ATR 动态止损 + 8. [v3.3] KDJ 随机指标 + 9. [v3.3] MACD/RSI 背离 + 10. [v3.3] OBV 量能 + 11. [v3.3] 威廉指标/乖离率 + 12. [v3.3] 支撑阻力位 + 13. [v3.4] K线形态 + 趋势形态 + """ + reasons = [] + buy_score = 0 # 正数倾向买入,负数倾向卖出 + + # === RSI 分析 === + if rsi < 30: + buy_score += 3 + reasons.append("✅ RSI 超卖 (<30),股价可能跌过头") + elif rsi < 40: + buy_score += 1 + reasons.append("📍 RSI 偏低,股价相对便宜") + elif rsi > 70: + buy_score -= 3 + reasons.append("⚠️ RSI 超买 (>70),股价可能涨过头") + elif rsi > 60: + buy_score -= 1 + reasons.append("📍 RSI 偏高,追高需谨慎") + + # === MACD 分析 === + if macd_histogram > 0 and prev_macd_histogram <= 0: + buy_score += 3 + reasons.append("✅ MACD 金叉,上涨动能启动") + elif macd_histogram < 0 and prev_macd_histogram >= 0: + buy_score -= 3 + reasons.append("⚠️ MACD 死叉,下跌动能启动") + elif macd_histogram > 0: + buy_score += 1 + reasons.append("📍 MACD 多头趋势") + else: + buy_score -= 1 + reasons.append("📍 MACD 空头趋势") + + # === 布林带分析 === + if current_price < bb_lower: + buy_score += 2 + reasons.append("✅ 跌破布林带下轨,可能超跌反弹") + elif current_price < bb_lower + (bb_middle - bb_lower) * 0.3: + buy_score += 1 + reasons.append("📍 接近布林带下轨,相对低位") + elif current_price > bb_upper: + buy_score -= 2 + reasons.append("⚠️ 突破布林带上轨,可能回调") + elif current_price > bb_upper - (bb_upper - bb_middle) * 0.3: + buy_score -= 1 + reasons.append("📍 接近布林带上轨,追高风险") + + # === KDJ 分析 (v3.3 新增) === + if kdj_signal: + if kdj_signal == 'golden_cross': + buy_score += 3 + reasons.append("✅ KDJ 金叉,短期买入信号") + elif kdj_signal == 'death_cross': + buy_score -= 3 + reasons.append("⚠️ KDJ 死叉,短期卖出信号") + elif kdj_signal in ['oversold', 'low_zone']: + buy_score += 2 + reasons.append("✅ KDJ 超卖,短期可能反弹") + elif kdj_signal in ['overbought', 'high_zone']: + buy_score -= 2 + reasons.append("⚠️ KDJ 超买,短期可能回调") + + # === 背离分析 (v3.3 新增) - 背离是强信号 === + if macd_divergence == 'bullish': + buy_score += 4 + reasons.append("🔥 MACD 底背离,强烈反弹信号") + elif macd_divergence == 'bearish': + buy_score -= 4 + reasons.append("🔥 MACD 顶背离,强烈回调信号") + + if rsi_divergence == 'bullish': + buy_score += 3 + reasons.append("✅ RSI 底背离,动能转强") + elif rsi_divergence == 'bearish': + buy_score -= 3 + reasons.append("⚠️ RSI 顶背离,动能转弱") + + # === OBV 分析 (v3.3 新增) === + if obv_signal: + if obv_signal == 'bullish_divergence': + buy_score += 2 + reasons.append("📊 OBV 底背离,资金悄悄流入") + elif obv_signal == 'bearish_divergence': + buy_score -= 2 + reasons.append("📊 OBV 顶背离,资金悄悄流出") + elif obv_signal == 'confirmed_up': + buy_score += 1 + reasons.append("📊 OBV 确认上涨趋势") + elif obv_signal == 'confirmed_down': + buy_score -= 1 + reasons.append("📊 OBV 确认下跌趋势") + + # === 威廉指标/乖离率 (v3.3 新增) === + if williams_signal == 'oversold': + buy_score += 1 + reasons.append("📍 威廉指标超卖") + elif williams_signal == 'overbought': + buy_score -= 1 + reasons.append("📍 威廉指标超买") + + if bias_signal == 'oversold': + buy_score += 1 + reasons.append("📍 乖离率偏低,可能反弹") + elif bias_signal == 'overbought': + buy_score -= 1 + reasons.append("📍 乖离率偏高,可能回调") + + # === 成交量分析 === + if volume_signal: + if volume_signal == "bullish" and volume_ratio and volume_ratio > 1.5: + buy_score += 2 + reasons.append(f"📊 放量上涨 (量比 {volume_ratio:.1f}),买盘积极") + elif volume_signal == "bearish" and volume_ratio and volume_ratio > 1.5: + buy_score -= 2 + reasons.append(f"📊 放量下跌 (量比 {volume_ratio:.1f}),卖压较大") + elif volume_signal == "neutral" and volume_ratio and volume_ratio < 0.7: + if macd_histogram < 0: + buy_score += 1 + reasons.append(f"📊 缩量下跌 (量比 {volume_ratio:.1f}),卖压减轻") + + # === 均线分析 === + if ma_arrangement: + if ma_arrangement == "多头排列": + buy_score += 2 + reasons.append("📈 均线多头排列,趋势向上") + elif ma_arrangement == "空头排列": + buy_score -= 2 + reasons.append("📉 均线空头排列,趋势向下") + + # === 趋势分析 === + if prices_1m and len(prices_1m) >= 5: + change_1m = (prices_1m[-1] - prices_1m[0]) / prices_1m[0] * 100 + if change_1m < -15: + buy_score += 1 + reasons.append(f"📍 近1月跌幅较大 ({change_1m:.1f}%),可能超跌") + elif change_1m > 15: + buy_score -= 1 + reasons.append(f"📍 近1月涨幅较大 ({change_1m:.1f}%),注意追高") + + # === 支撑阻力位分析 (v3.3 新增) === + if nearest_support and nearest_resistance: + support_distance = (current_price - nearest_support) / current_price * 100 + resist_distance = (nearest_resistance - current_price) / current_price * 100 + + if support_distance < 3: # 接近支撑位 + buy_score += 1 + reasons.append(f"📍 接近支撑位 ${nearest_support:.2f},可能有支撑") + if resist_distance < 3: # 接近阻力位 + buy_score -= 1 + reasons.append(f"📍 接近阻力位 ${nearest_resistance:.2f},可能有压力") + + # === 形态识别分析 (v3.4 新增) === + if patterns_data: + all_patterns = patterns_data.get('all_patterns', []) + + # 按形态强度和类型评分 + for pattern in all_patterns: + p_signal = pattern.get('signal', 'neutral') + p_strength = pattern.get('strength', 'medium') + p_name = pattern.get('pattern', '') + + # 强信号形态(三只白兵/乌鸦、早晨/黄昏之星、头肩、双底双顶) + strong_patterns = ['three_white_soldiers', 'three_black_crows', + 'morning_star', 'evening_star', + 'head_and_shoulders_top', 'head_and_shoulders_bottom', + 'double_bottom', 'double_top'] + + # 中等信号形态 + medium_patterns = ['bullish_engulfing', 'bearish_engulfing', + 'ascending_triangle', 'descending_triangle'] + + if p_name in strong_patterns: + if p_signal == 'bullish': + buy_score += 3 + cn_name = CANDLESTICK_NAMES.get(p_name) or CHART_PATTERN_NAMES.get(p_name, p_name) + reasons.append(f"🔥 {cn_name}形态,强看涨信号") + elif p_signal == 'bearish': + buy_score -= 3 + cn_name = CANDLESTICK_NAMES.get(p_name) or CHART_PATTERN_NAMES.get(p_name, p_name) + reasons.append(f"🔥 {cn_name}形态,强看跌信号") + elif p_name in medium_patterns: + if p_signal == 'bullish': + buy_score += 2 + cn_name = CANDLESTICK_NAMES.get(p_name) or CHART_PATTERN_NAMES.get(p_name, p_name) + reasons.append(f"✅ {cn_name}形态,看涨信号") + elif p_signal == 'bearish': + buy_score -= 2 + cn_name = CANDLESTICK_NAMES.get(p_name) or CHART_PATTERN_NAMES.get(p_name, p_name) + reasons.append(f"⚠️ {cn_name}形态,看跌信号") + else: + # 弱信号形态(锤子线、十字星等) + if p_signal == 'bullish': + buy_score += 1 + cn_name = CANDLESTICK_NAMES.get(p_name) or CHART_PATTERN_NAMES.get(p_name, p_name) + reasons.append(f"📍 {cn_name}形态出现") + elif p_signal == 'bearish': + buy_score -= 1 + cn_name = CANDLESTICK_NAMES.get(p_name) or CHART_PATTERN_NAMES.get(p_name, p_name) + reasons.append(f"📍 {cn_name}形态出现") + + # === 生成建议 (v3.4 调整阈值) === + if buy_score >= 6: + action = "BUY" + confidence = "高" + elif buy_score >= 3: + action = "BUY" + confidence = "中" + elif buy_score <= -6: + action = "SELL" + confidence = "高" + elif buy_score <= -3: + action = "SELL" + confidence = "中" + else: + action = "HOLD" + confidence = "中" + + # === 计算价格建议 === + if atr and atr_percent: + if atr_percent > 5: + atr_multiplier = 2.5 # 高波动 + elif atr_percent > 3: + atr_multiplier = 2.0 # 中等波动 + else: + atr_multiplier = 1.5 # 低波动 + + stop_loss = current_price - atr * atr_multiplier + take_profit = current_price + atr * atr_multiplier * 2.5 + buy_price = current_price - atr * 0.5 + risk = current_price - stop_loss + reward = take_profit - current_price + risk_reward = reward / risk if risk > 0 else 0 + else: + buy_price = min(current_price * 0.97, bb_lower * 1.02) + stop_loss = bb_lower * 0.95 + take_profit = bb_upper * 0.95 + risk_reward = None + + # 结合支撑位优化止损 + if nearest_support and stop_loss > nearest_support: + stop_loss = nearest_support * 0.98 # 支撑位下方 2% + + sell_price = max(current_price * 1.05, bb_upper * 0.98) + + # 结合阻力位优化止盈 + if nearest_resistance and take_profit > nearest_resistance: + take_profit = nearest_resistance * 0.98 # 阻力位下方 2% + + # 仓位建议 + if confidence == "高": + suggested_position = 30.0 + elif confidence == "中": + suggested_position = 20.0 + else: + suggested_position = 10.0 + + # 确定主要背离信号 + divergence_signal = None + if macd_divergence in ['bullish', 'bearish']: + divergence_signal = f"MACD_{macd_divergence}" + elif rsi_divergence in ['bullish', 'bearish']: + divergence_signal = f"RSI_{rsi_divergence}" + + return TradingSignal( + action=action, + confidence=confidence, + buy_price=round(buy_price, 2), + sell_price=round(sell_price, 2), + stop_loss=round(stop_loss, 2), + take_profit=round(take_profit, 2), + reasons=reasons, + # v3.2 字段 + atr=atr, + atr_percent=atr_percent, + risk_reward_ratio=round(risk_reward, 2) if risk_reward else None, + suggested_position=suggested_position, + volume_signal=volume_signal, + ma_trend=ma_arrangement, + # v3.3 新增字段 + kdj_signal=kdj_signal, + divergence_signal=divergence_signal, + obv_signal=obv_signal, + support_price=nearest_support, + resistance_price=nearest_resistance, + score=buy_score + ) + + +def format_simple_report( + ticker: str, + name: str, + current_price: float, + change_pct: float, + rsi: float, + macd_histogram: float, + bb_upper: float, + bb_middle: float, + bb_lower: float, + signal: TradingSignal +) -> str: + """生成简洁版报告(一屏看完)""" + + # 涨跌颜色 + change_emoji = "🟢" if change_pct >= 0 else "🔴" + + # 信号颜色 + if signal.action == "BUY": + action_text = "🟢 **建议买入**" + elif signal.action == "SELL": + action_text = "🔴 **建议卖出**" + else: + action_text = "⚪ **观望等待**" + + report = f""" +## {ticker} ({name}) 简易分析 + +### 📊 当前状态 +- **价格**: ${current_price:.2f} ({change_emoji} {change_pct:+.2f}%) +- **RSI**: {rsi:.1f} {'🟢超卖' if rsi < 30 else '🔴超买' if rsi > 70 else '⚪正常'} +- **MACD**: {'🟢多头' if macd_histogram > 0 else '🔴空头'} +- **位置**: 在布林带 {'下方' if current_price < bb_lower else '上方' if current_price > bb_upper else '中间'} + +### 🎯 交易建议 +{action_text} (置信度: {signal.confidence}) + +| 操作 | 建议价格 | +|------|----------| +| 买入价 | ${signal.buy_price:.2f} | +| 止损价 | ${signal.stop_loss:.2f} | +| 止盈价 | ${signal.take_profit:.2f} | + +### 📝 理由 +""" + + for reason in signal.reasons[:4]: # 最多显示4条 + report += f"- {reason}\n" + + return report + + +def format_detailed_report( + ticker: str, + name: str, + current_price: float, + change_pct: float, + rsi: float, + macd_line: float, + signal_line: float, + macd_histogram: float, + prev_macd_histogram: float, + bb_upper: float, + bb_middle: float, + bb_lower: float, + prices_1m: List[float], + prices_3m: List[float], + signal: TradingSignal +) -> str: + """生成详细版报告""" + + # 基础信息 + change_emoji = "🟢" if change_pct >= 0 else "🔴" + + report = f""" +# {ticker} ({name}) 详细技术分析 + +--- + +## 一、价格概览 + +| 指标 | 数值 | +|------|------| +| 当前价格 | ${current_price:.2f} | +| 今日涨跌 | {change_emoji} {change_pct:+.2f}% | +| 布林带上轨 | ${bb_upper:.2f} | +| 布林带中轨 | ${bb_middle:.2f} | +| 布林带下轨 | ${bb_lower:.2f} | + +--- + +## 二、技术指标解读 + +### RSI (相对强弱指数) +{explain_rsi_simple(rsi)} + +### MACD (指数平滑异同移动平均线) +{explain_macd_simple(macd_line, signal_line, macd_histogram, prev_macd_histogram)} + +| MACD 数值 | | +|-----------|--------| +| MACD 线 | {macd_line:.4f} | +| 信号线 | {signal_line:.4f} | +| 柱状图 | {macd_histogram:.4f} | + +### 布林带位置 +{explain_bollinger_simple(current_price, bb_upper, bb_middle, bb_lower)} + +--- + +## 三、趋势分析 + +### 近1个月趋势 +{explain_trend_simple(prices_1m, "近1个月") if prices_1m else "数据不足"} + +### 近3个月趋势 +{explain_trend_simple(prices_3m, "近3个月") if prices_3m else "数据不足"} + +--- + +## 四、交易建议 + +### 综合判断 +""" + + if signal.action == "BUY": + report += "### 🟢 **建议买入**\n\n" + elif signal.action == "SELL": + report += "### 🔴 **建议卖出**\n\n" + else: + report += "### ⚪ **建议观望**\n\n" + + report += f"**置信度**: {signal.confidence}\n\n" + + report += "**分析理由**:\n" + for i, reason in enumerate(signal.reasons, 1): + report += f"{i}. {reason}\n" + + report += f""" +### 价格建议 + +| 操作类型 | 建议价格 | 说明 | +|----------|----------|------| +| 🟢 买入价 | ${signal.buy_price:.2f} | 建议在此价格附近分批买入 | +| 🔴 止损价 | ${signal.stop_loss:.2f} | 跌破此价应止损离场 | +| 🎯 止盈价 | ${signal.take_profit:.2f} | 涨到此价可考虑部分止盈 | + +--- + +## 五、风险提示 + +⚠️ **重要提醒**: +1. 以上分析仅供参考,不构成投资建议 +2. 股市有风险,投资需谨慎 +3. 建议分批建仓,不要一次性满仓 +4. 设置好止损,控制风险 +5. 技术分析有局限性,需结合基本面和市场环境 + +--- + +*分析时间: {datetime.now().strftime('%Y-%m-%d %H:%M')}* +""" + + return report diff --git a/skills/stock-master/scripts/feishu_init_tables.py b/skills/stock-master/scripts/feishu_init_tables.py new file mode 100644 index 00000000..ae1d100c --- /dev/null +++ b/skills/stock-master/scripts/feishu_init_tables.py @@ -0,0 +1,228 @@ +""" +飞书多维表格初始化脚本 + +功能:创建 Stock Master 所需的表结构 +""" + +import json +import requests +from feishu_sync import FeishuBitable + + +def create_field(bitable: FeishuBitable, field_name: str, field_type: int, + table_id: str = None, **kwargs) -> dict: + """ + 创建字段 + + 字段类型: + 1: 文本 + 2: 数字 + 3: 单选 + 4: 多选 + 5: 日期 + 7: 复选框 + """ + table_id = table_id or bitable.table_id + endpoint = f"/bitable/v1/apps/{bitable.app_token}/tables/{table_id}/fields" + + data = { + "field_name": field_name, + "type": field_type + } + + # 单选/多选需要配置选项 + if field_type in [3, 4] and 'options' in kwargs: + data['property'] = { + 'options': [{'name': opt} for opt in kwargs['options']] + } + + # 数字字段配置 + if field_type == 2 and 'formatter' in kwargs: + data['property'] = {'formatter': kwargs['formatter']} + + return bitable._request('POST', endpoint, data) + + +def init_signal_table(bitable: FeishuBitable, table_id: str = None): + """ + 初始化技术信号表字段 + + 表结构: + - 股票代码 (文本) + - 股票名称 (文本) + - 当前价格 (数字) + - 综合评分 (数字) + - 操作建议 (单选: 强烈买入/建议买入/观望/建议卖出/强烈卖出) + - RSI (数字) + - MACD信号 (单选: 金叉/多头/空头/死叉) + - KDJ信号 (单选: 金叉/超卖/中性/超买/死叉) + - 背离信号 (单选: 无/底背离/顶背离) + - 形态信号 (多选: 锤子线/吞没/十字星/双底/双顶/头肩/三角形...) + - 止损价 (数字) + - 止盈价 (数字) + - 分析理由 (文本) + - 更新时间 (日期) + """ + table_id = table_id or bitable.table_id + + fields_config = [ + ("股票代码", 1, {}), + ("股票名称", 1, {}), + ("当前价格", 2, {"formatter": "0.00"}), + ("综合评分", 2, {"formatter": "0"}), + ("操作建议", 3, {"options": ["强烈买入", "建议买入", "观望", "建议卖出", "强烈卖出"]}), + ("RSI", 2, {"formatter": "0.0"}), + ("MACD信号", 3, {"options": ["金叉", "多头", "空头", "死叉"]}), + ("KDJ信号", 3, {"options": ["金叉", "超卖", "中性", "超买", "死叉"]}), + ("背离信号", 3, {"options": ["无", "底背离", "顶背离"]}), + ("形态信号", 4, {"options": [ + "锤子线", "上吊线", "十字星", "看涨吞没", "看跌吞没", + "早晨之星", "黄昏之星", "三只白兵", "三只乌鸦", + "双底", "双顶", "头肩顶", "头肩底", + "上升三角形", "下降三角形", "对称三角形" + ]}), + ("止损价", 2, {"formatter": "0.00"}), + ("止盈价", 2, {"formatter": "0.00"}), + ("分析理由", 1, {}), + ("更新时间", 5, {}), + ] + + print(f"正在初始化表 {table_id} 的字段...") + + success = 0 + failed = 0 + + for field_name, field_type, kwargs in fields_config: + try: + create_field(bitable, field_name, field_type, table_id, **kwargs) + print(f" ✓ 创建字段: {field_name}") + success += 1 + except Exception as e: + if "already exist" in str(e).lower() or "已存在" in str(e): + print(f" - 字段已存在: {field_name}") + else: + print(f" ✗ 创建失败: {field_name} - {e}") + failed += 1 + + print(f"\n完成! 成功: {success}, 失败: {failed}") + return {"success": success, "failed": failed} + + +def create_holdings_table(bitable: FeishuBitable) -> str: + """ + 创建持仓表 + + 返回新表的 table_id + """ + fields = [ + {"field_name": "股票代码", "type": 1}, + {"field_name": "股票名称", "type": 1}, + {"field_name": "市场", "type": 3, "property": {"options": [ + {"name": "美股"}, {"name": "港股"}, {"name": "A股"} + ]}}, + {"field_name": "持仓数量", "type": 2}, + {"field_name": "成本价", "type": 2, "property": {"formatter": "0.00"}}, + {"field_name": "当前价", "type": 2, "property": {"formatter": "0.00"}}, + {"field_name": "市值", "type": 2, "property": {"formatter": "0.00"}}, + {"field_name": "盈亏金额", "type": 2, "property": {"formatter": "0.00"}}, + {"field_name": "盈亏比例", "type": 2, "property": {"formatter": "0.00%"}}, + {"field_name": "买入日期", "type": 5}, + {"field_name": "备注", "type": 1}, + ] + + endpoint = f"/bitable/v1/apps/{bitable.app_token}/tables" + data = { + "table": { + "name": "持仓管理", + "default_view_name": "全部持仓", + "fields": fields + } + } + + result = bitable._request('POST', endpoint, data) + table_id = result.get('table_id') + print(f"创建持仓表成功: {table_id}") + return table_id + + +def create_trades_table(bitable: FeishuBitable) -> str: + """ + 创建交易记录表 + + 返回新表的 table_id + """ + fields = [ + {"field_name": "股票代码", "type": 1}, + {"field_name": "交易类型", "type": 3, "property": {"options": [ + {"name": "买入"}, {"name": "卖出"}, {"name": "做T买"}, {"name": "做T卖"} + ]}}, + {"field_name": "交易价格", "type": 2, "property": {"formatter": "0.00"}}, + {"field_name": "交易数量", "type": 2}, + {"field_name": "交易金额", "type": 2, "property": {"formatter": "0.00"}}, + {"field_name": "交易时间", "type": 5}, + {"field_name": "触发信号", "type": 1}, + {"field_name": "备注", "type": 1}, + ] + + endpoint = f"/bitable/v1/apps/{bitable.app_token}/tables" + data = { + "table": { + "name": "交易记录", + "default_view_name": "全部记录", + "fields": fields + } + } + + result = bitable._request('POST', endpoint, data) + table_id = result.get('table_id') + print(f"创建交易记录表成功: {table_id}") + return table_id + + +def init_all_tables(config_path: str = None): + """ + 一键初始化所有表结构 + """ + bitable = FeishuBitable(config_path) + + print("=" * 50) + print("Stock Master 飞书多维表格初始化") + print("=" * 50) + + # 1. 初始化主表(技术信号表)字段 + print("\n[1/3] 初始化技术信号表...") + init_signal_table(bitable) + + # 2. 创建持仓表 + print("\n[2/3] 创建持仓管理表...") + try: + holdings_table_id = create_holdings_table(bitable) + except Exception as e: + if "already exist" in str(e).lower(): + print(" 持仓表已存在") + else: + print(f" 创建失败: {e}") + + # 3. 创建交易记录表 + print("\n[3/3] 创建交易记录表...") + try: + trades_table_id = create_trades_table(bitable) + except Exception as e: + if "already exist" in str(e).lower(): + print(" 交易记录表已存在") + else: + print(f" 创建失败: {e}") + + print("\n" + "=" * 50) + print("初始化完成!") + print("=" * 50) + + # 列出所有表 + tables = bitable.list_tables() + print("\n当前表列表:") + for t in tables: + print(f" - {t['name']} (ID: {t['table_id']})") + + +if __name__ == "__main__": + init_all_tables() diff --git a/skills/stock-master/scripts/feishu_sync.py b/skills/stock-master/scripts/feishu_sync.py new file mode 100644 index 00000000..11233530 --- /dev/null +++ b/skills/stock-master/scripts/feishu_sync.py @@ -0,0 +1,492 @@ +""" +飞书多维表格同步模块 v1.0 + +功能: +- 连接飞书多维表格 +- 同步持仓数据 +- 同步技术信号 +- 同步交易记录 + +数据流向:本地分析结果 → 飞书多维表格(单向同步) + +配置说明: +请创建 feishu_config.json 文件,格式参考 feishu_config.example.json +""" + +import json +import requests +from typing import Dict, List, Optional, Any, Union +from datetime import datetime +from pathlib import Path +import time +import os + + +def get_config_path() -> Path: + """获取配置文件路径""" + # 优先使用环境变量 + if os.environ.get('FEISHU_CONFIG_PATH'): + return Path(os.environ['FEISHU_CONFIG_PATH']) + + # 其次使用当前目录 + local_config = Path('./feishu_config.json') + if local_config.exists(): + return local_config + + # 最后使用用户目录 + home_config = Path.home() / '.stock-master' / 'feishu_config.json' + if home_config.exists(): + return home_config + + raise FileNotFoundError( + "找不到飞书配置文件。请创建 feishu_config.json," + "或设置环境变量 FEISHU_CONFIG_PATH" + ) + + +def to_feishu_timestamp(dt: Union[str, datetime, int, None]) -> Optional[int]: + """ + 将日期时间转换为飞书多维表格需要的毫秒时间戳 + + 参数: + dt: 日期时间,可以是: + - datetime 对象 + - 字符串 (格式: 'YYYY-MM-DD' 或 'YYYY-MM-DD HH:MM:SS') + - 毫秒时间戳 (int) + - None + + 返回: + 毫秒时间戳 或 None + """ + if dt is None: + return None + + if isinstance(dt, int): + # 已经是时间戳,检查是秒还是毫秒 + if dt > 1e12: # 已经是毫秒 + return dt + return dt * 1000 # 秒转毫秒 + + if isinstance(dt, datetime): + return int(dt.timestamp() * 1000) + + if isinstance(dt, str): + # 尝试解析字符串 + formats = [ + '%Y-%m-%d %H:%M:%S', + '%Y-%m-%d %H:%M', + '%Y-%m-%d', + '%Y/%m/%d %H:%M:%S', + '%Y/%m/%d' + ] + for fmt in formats: + try: + parsed = datetime.strptime(dt, fmt) + return int(parsed.timestamp() * 1000) + except ValueError: + continue + # 解析失败,返回当前时间 + return int(time.time() * 1000) + + return int(time.time() * 1000) + + +class FeishuBitable: + """飞书多维表格 API 封装""" + + BASE_URL = "https://open.feishu.cn/open-apis" + + def __init__(self, config_path: str = None): + """ + 初始化飞书连接 + + 参数: + config_path: 配置文件路径,如不指定则自动查找 + """ + if config_path is None: + config_path = get_config_path() + else: + config_path = Path(config_path) + + if not config_path.exists(): + raise FileNotFoundError(f"配置文件不存在: {config_path}") + + with open(config_path, 'r', encoding='utf-8') as f: + config = json.load(f) + + self.app_id = config['APP_ID'] + self.app_secret = config['APP_SECRET'] + self.app_token = config['APP_TOKEN'] + self.table_id = config['TABLE_ID'] + self._tenant_access_token = None + self._token_expires_at = 0 + + def _get_tenant_access_token(self) -> str: + """获取 tenant_access_token""" + import time + + # 检查 token 是否过期 + if self._tenant_access_token and time.time() < self._token_expires_at - 60: + return self._tenant_access_token + + url = f"{self.BASE_URL}/auth/v3/tenant_access_token/internal" + payload = { + "app_id": self.app_id, + "app_secret": self.app_secret + } + + response = requests.post(url, json=payload) + result = response.json() + + if result.get('code') != 0: + raise Exception(f"获取 token 失败: {result.get('msg')}") + + self._tenant_access_token = result['tenant_access_token'] + self._token_expires_at = time.time() + result.get('expire', 7200) + + return self._tenant_access_token + + def _request(self, method: str, endpoint: str, data: dict = None) -> dict: + """发送 API 请求""" + url = f"{self.BASE_URL}{endpoint}" + headers = { + "Authorization": f"Bearer {self._get_tenant_access_token()}", + "Content-Type": "application/json" + } + + if method.upper() == 'GET': + response = requests.get(url, headers=headers, params=data) + elif method.upper() == 'POST': + response = requests.post(url, headers=headers, json=data) + elif method.upper() == 'PUT': + response = requests.put(url, headers=headers, json=data) + elif method.upper() == 'DELETE': + response = requests.delete(url, headers=headers, json=data) + else: + raise ValueError(f"不支持的 HTTP 方法: {method}") + + result = response.json() + + if result.get('code') != 0: + raise Exception(f"API 请求失败: {result.get('msg')}") + + return result.get('data', {}) + + # ========================================== + # 表格操作 + # ========================================== + + def list_tables(self) -> List[dict]: + """列出多维表格中的所有数据表""" + endpoint = f"/bitable/v1/apps/{self.app_token}/tables" + result = self._request('GET', endpoint) + return result.get('items', []) + + def get_table_fields(self, table_id: str = None) -> List[dict]: + """获取数据表的字段列表""" + table_id = table_id or self.table_id + endpoint = f"/bitable/v1/apps/{self.app_token}/tables/{table_id}/fields" + result = self._request('GET', endpoint) + return result.get('items', []) + + def create_table(self, name: str, fields: List[dict]) -> dict: + """ + 创建新数据表 + + 参数: + name: 表名 + fields: 字段定义列表 + """ + endpoint = f"/bitable/v1/apps/{self.app_token}/tables" + data = { + "table": { + "name": name, + "default_view_name": "默认视图", + "fields": fields + } + } + return self._request('POST', endpoint, data) + + # ========================================== + # 记录操作 + # ========================================== + + def list_records(self, table_id: str = None, page_size: int = 100, + filter_str: str = None) -> List[dict]: + """ + 获取数据表记录 + + 参数: + table_id: 表 ID + page_size: 每页数量 + filter_str: 筛选条件 + """ + table_id = table_id or self.table_id + endpoint = f"/bitable/v1/apps/{self.app_token}/tables/{table_id}/records" + + params = {"page_size": page_size} + if filter_str: + params["filter"] = filter_str + + result = self._request('GET', endpoint, params) + return result.get('items', []) + + def create_record(self, fields: dict, table_id: str = None) -> dict: + """ + 创建单条记录 + + 参数: + fields: 字段值字典 + table_id: 表 ID + """ + table_id = table_id or self.table_id + endpoint = f"/bitable/v1/apps/{self.app_token}/tables/{table_id}/records" + data = {"fields": fields} + return self._request('POST', endpoint, data) + + def batch_create_records(self, records: List[dict], table_id: str = None) -> dict: + """ + 批量创建记录 + + 参数: + records: 记录列表,每条记录为 {"fields": {...}} + table_id: 表 ID + """ + table_id = table_id or self.table_id + endpoint = f"/bitable/v1/apps/{self.app_token}/tables/{table_id}/records/batch_create" + data = {"records": [{"fields": r} if "fields" not in r else r for r in records]} + return self._request('POST', endpoint, data) + + def update_record(self, record_id: str, fields: dict, table_id: str = None) -> dict: + """ + 更新单条记录 + + 参数: + record_id: 记录 ID + fields: 要更新的字段值 + table_id: 表 ID + """ + table_id = table_id or self.table_id + endpoint = f"/bitable/v1/apps/{self.app_token}/tables/{table_id}/records/{record_id}" + data = {"fields": fields} + return self._request('PUT', endpoint, data) + + def delete_record(self, record_id: str, table_id: str = None) -> dict: + """删除单条记录""" + table_id = table_id or self.table_id + endpoint = f"/bitable/v1/apps/{self.app_token}/tables/{table_id}/records/{record_id}" + return self._request('DELETE', endpoint) + + def find_record_by_field(self, field_name: str, value: str, + table_id: str = None) -> Optional[dict]: + """ + 根据字段值查找记录 + + 参数: + field_name: 字段名 + value: 字段值 + table_id: 表 ID + + 返回: + 找到的记录或 None + """ + records = self.list_records(table_id) + for record in records: + if record.get('fields', {}).get(field_name) == value: + return record + return None + + +# ========================================== +# Stock Master 同步函数 +# ========================================== + +def sync_stock_signal(bitable: FeishuBitable, signal_data: dict, + table_id: str = None) -> dict: + """ + 同步单只股票的技术信号到飞书 + + 参数: + bitable: FeishuBitable 实例 + signal_data: 股票信号数据,格式: + { + 'ticker': 'AAPL', + 'name': '苹果', + 'current_price': 185.5, + 'score': 5, + 'action': 'BUY', + 'rsi': 45.2, + 'macd_signal': '金叉', + 'kdj_signal': '超卖', + 'patterns': ['锤子线', '双底'], + 'stop_loss': 175.0, + 'take_profit': 200.0, + 'reasons': ['RSI 超卖', 'MACD 金叉'], + 'timestamp': '2026-01-22 15:30:00' + } + table_id: 表 ID + + 返回: + 创建或更新的记录 + """ + # 转换数据格式为飞书字段格式 + fields = { + "股票代码": signal_data.get('ticker', ''), + "股票名称": signal_data.get('name', ''), + "当前价格": signal_data.get('current_price', 0), + "综合评分": signal_data.get('score', 0), + "操作建议": signal_data.get('action', 'HOLD'), + "RSI": signal_data.get('rsi', 50), + "MACD信号": signal_data.get('macd_signal', ''), + "KDJ信号": signal_data.get('kdj_signal', ''), + "背离信号": signal_data.get('divergence', '无'), + "形态信号": signal_data.get('patterns', []), # 多选字段 + "止损价": signal_data.get('stop_loss', 0), + "止盈价": signal_data.get('take_profit', 0), + "分析理由": '\n'.join(signal_data.get('reasons', [])), + "更新时间": to_feishu_timestamp(signal_data.get('timestamp')) + } + + # 查找是否已存在该股票的记录 + existing = bitable.find_record_by_field("股票代码", fields["股票代码"], table_id) + + if existing: + # 更新现有记录 + record_id = existing['record_id'] + return bitable.update_record(record_id, fields, table_id) + else: + # 创建新记录 + return bitable.create_record(fields, table_id) + + +def sync_holding(bitable: FeishuBitable, holding_data: dict, + table_id: str = None) -> dict: + """ + 同步持仓数据到飞书 + + 参数: + holding_data: 持仓数据 + """ + fields = { + "股票代码": holding_data.get('ticker', ''), + "股票名称": holding_data.get('name', ''), + "持仓数量": holding_data.get('quantity', 0), + "成本价": holding_data.get('cost_price', 0), + "当前价": holding_data.get('current_price', 0), + "盈亏金额": holding_data.get('profit_amount', 0), + "盈亏比例": holding_data.get('profit_ratio', 0), + "市场": holding_data.get('market', '美股'), + "买入日期": to_feishu_timestamp(holding_data.get('buy_date')), + "备注": holding_data.get('note', '') + } + + existing = bitable.find_record_by_field("股票代码", fields["股票代码"], table_id) + + if existing: + return bitable.update_record(existing['record_id'], fields, table_id) + else: + return bitable.create_record(fields, table_id) + + +def sync_trade_record(bitable: FeishuBitable, trade_data: dict, + table_id: str = None) -> dict: + """ + 同步交易记录到飞书(追加模式) + + 参数: + trade_data: 交易数据 + """ + fields = { + "股票代码": trade_data.get('ticker', ''), + "交易类型": trade_data.get('trade_type', '买入'), # 买入/卖出/做T + "交易价格": trade_data.get('price', 0), + "交易数量": trade_data.get('quantity', 0), + "交易金额": trade_data.get('amount', 0), + "交易时间": to_feishu_timestamp(trade_data.get('timestamp')), + "触发信号": trade_data.get('signal', ''), + "备注": trade_data.get('note', '') + } + + # 交易记录始终追加,不覆盖 + return bitable.create_record(fields, table_id) + + +def batch_sync_signals(bitable: FeishuBitable, signals: List[dict], + table_id: str = None) -> dict: + """ + 批量同步股票信号 + + 参数: + signals: 信号数据列表 + """ + results = { + 'success': 0, + 'failed': 0, + 'errors': [] + } + + for signal in signals: + try: + sync_stock_signal(bitable, signal, table_id) + results['success'] += 1 + except Exception as e: + results['failed'] += 1 + results['errors'].append({ + 'ticker': signal.get('ticker'), + 'error': str(e) + }) + + return results + + +# ========================================== +# 便捷函数 +# ========================================== + +def quick_sync_signal(signal_data: dict, config_path: str = None) -> dict: + """ + 快速同步单只股票信号(一行调用) + + 示例: + quick_sync_signal({ + 'ticker': 'AAPL', + 'score': 5, + 'action': 'BUY', + 'rsi': 45, + ... + }) + """ + bitable = FeishuBitable(config_path) + return sync_stock_signal(bitable, signal_data) + + +def test_connection(config_path: str = None) -> dict: + """ + 测试飞书连接 + + 返回: + {'status': 'ok', 'tables': [...]} 或错误信息 + """ + try: + bitable = FeishuBitable(config_path) + tables = bitable.list_tables() + return { + 'status': 'ok', + 'message': '连接成功', + 'tables': [{'id': t['table_id'], 'name': t['name']} for t in tables] + } + except Exception as e: + return { + 'status': 'error', + 'message': str(e) + } + + +# ========================================== +# 测试 +# ========================================== + +if __name__ == "__main__": + print("测试飞书连接...") + result = test_connection() + print(json.dumps(result, ensure_ascii=False, indent=2)) diff --git a/skills/stock-master/scripts/indicators.py b/skills/stock-master/scripts/indicators.py new file mode 100644 index 00000000..b64f9b6e --- /dev/null +++ b/skills/stock-master/scripts/indicators.py @@ -0,0 +1,1783 @@ +""" +技术指标本地计算模块 v3.4 + +功能: +- 本地计算 RSI、布林带、ATR、均线等 +- 支持港股(当 Alpha Vantage 不可用时) +- 提供优雅降级能力 + +v3.4 新增: +- K线形态识别(锤子线、吞没、十字星、早晨之星等) +- 趋势形态识别(双底、双顶、头肩顶、三角形等) + +v3.3: +- KDJ 随机指标 +- MACD/RSI 背离检测 +- 增强版支撑阻力位(斐波那契) +- OBV 量能指标 +- 威廉指标 Williams %R +- 乖离率 BIAS +""" + +from typing import Dict, List, Optional, Any, Tuple +import numpy as np +from datetime import datetime + + +def is_hk_stock(ticker: str) -> bool: + """判断是否是港股""" + return ticker.upper().endswith('.HK') + + +def is_cn_stock(ticker: str) -> bool: + """判断是否是A股""" + return ticker.upper().endswith('.SS') or ticker.upper().endswith('.SZ') + + +def get_stock_data(ticker: str, period: str = '3mo') -> Dict[str, Any]: + """ + 获取股票历史数据 + + 返回: + 包含 close, high, low, volume 等数组的字典 + """ + try: + import yfinance as yf + stock = yf.Ticker(ticker) + hist = stock.history(period=period) + + if hist.empty: + return {'error': f'无法获取 {ticker} 的数据'} + + return { + 'ticker': ticker.upper(), + 'close': hist['Close'].values, + 'high': hist['High'].values, + 'low': hist['Low'].values, + 'open': hist['Open'].values, + 'volume': hist['Volume'].values, + 'dates': hist.index.tolist(), + 'current_price': float(hist['Close'].iloc[-1]), + 'source': 'Yahoo Finance' + } + except Exception as e: + return {'error': str(e)} + + +# ============================================ +# RSI 本地计算 +# ============================================ + +def calculate_rsi(prices: np.ndarray, period: int = 14) -> float: + """ + 计算 RSI (相对强弱指数) + + 参数: + prices: 收盘价数组 + period: RSI 周期,默认 14 + + 返回: + 最新的 RSI 值 + """ + if len(prices) < period + 1: + return 50.0 # 数据不足时返回中性值 + + # 计算价格变动 + deltas = np.diff(prices) + + # 分离涨跌 + gains = np.where(deltas > 0, deltas, 0) + losses = np.where(deltas < 0, -deltas, 0) + + # 使用 EMA 方式计算平均涨跌(更平滑) + alpha = 1.0 / period + + avg_gain = gains[0] + avg_loss = losses[0] + + for i in range(1, len(gains)): + avg_gain = alpha * gains[i] + (1 - alpha) * avg_gain + avg_loss = alpha * losses[i] + (1 - alpha) * avg_loss + + if avg_loss == 0: + return 100.0 + + rs = avg_gain / avg_loss + rsi = 100 - (100 / (1 + rs)) + + return round(rsi, 2) + + +def calculate_rsi_series(prices: np.ndarray, period: int = 14) -> np.ndarray: + """计算 RSI 序列(用于判断趋势)""" + if len(prices) < period + 1: + return np.array([50.0]) + + deltas = np.diff(prices) + gains = np.where(deltas > 0, deltas, 0) + losses = np.where(deltas < 0, -deltas, 0) + + alpha = 1.0 / period + rsi_values = [] + + avg_gain = np.mean(gains[:period]) + avg_loss = np.mean(losses[:period]) + + for i in range(period, len(gains)): + avg_gain = alpha * gains[i] + (1 - alpha) * avg_gain + avg_loss = alpha * losses[i] + (1 - alpha) * avg_loss + + if avg_loss == 0: + rsi_values.append(100.0) + else: + rs = avg_gain / avg_loss + rsi_values.append(100 - (100 / (1 + rs))) + + return np.array(rsi_values) + + +# ============================================ +# 布林带本地计算 +# ============================================ + +def calculate_bollinger_bands( + prices: np.ndarray, + period: int = 20, + std_dev: float = 2.0 +) -> Dict[str, float]: + """ + 计算布林带 + + 参数: + prices: 收盘价数组 + period: 移动平均周期,默认 20 + std_dev: 标准差倍数,默认 2 + + 返回: + 包含 upper, middle, lower 的字典 + """ + if len(prices) < period: + return {'error': '数据不足'} + + # 计算中轨(SMA) + middle = np.mean(prices[-period:]) + + # 计算标准差 + std = np.std(prices[-period:]) + + # 计算上下轨 + upper = middle + std_dev * std + lower = middle - std_dev * std + + return { + 'upper': round(upper, 2), + 'middle': round(middle, 2), + 'lower': round(lower, 2), + 'bandwidth': round((upper - lower) / middle * 100, 2), # 带宽百分比 + 'source': 'Local Calculation' + } + + +# ============================================ +# ATR (平均真实波幅) 计算 +# ============================================ + +def calculate_atr( + high: np.ndarray, + low: np.ndarray, + close: np.ndarray, + period: int = 14 +) -> float: + """ + 计算 ATR (Average True Range) + + ATR 反映股票的波动性,用于动态止损计算 + + 参数: + high: 最高价数组 + low: 最低价数组 + close: 收盘价数组 + period: ATR 周期,默认 14 + + 返回: + 最新的 ATR 值 + """ + if len(high) < period + 1: + return 0.0 + + # 计算 True Range + tr_list = [] + for i in range(1, len(high)): + # TR = max(高-低, |高-昨收|, |低-昨收|) + tr = max( + high[i] - low[i], + abs(high[i] - close[i-1]), + abs(low[i] - close[i-1]) + ) + tr_list.append(tr) + + tr_array = np.array(tr_list) + + # 使用 EMA 计算 ATR + alpha = 1.0 / period + atr = tr_array[0] + + for i in range(1, len(tr_array)): + atr = alpha * tr_array[i] + (1 - alpha) * atr + + return round(atr, 4) + + +def calculate_atr_percent( + high: np.ndarray, + low: np.ndarray, + close: np.ndarray, + period: int = 14 +) -> float: + """计算 ATR 百分比(相对于当前价格)""" + atr = calculate_atr(high, low, close, period) + current_price = close[-1] + + if current_price == 0: + return 0.0 + + return round(atr / current_price * 100, 2) + + +# ============================================ +# 均线系统 +# ============================================ + +def calculate_ma(prices: np.ndarray, period: int) -> float: + """计算简单移动平均线""" + if len(prices) < period: + return prices[-1] if len(prices) > 0 else 0 + return round(np.mean(prices[-period:]), 2) + + +def calculate_ema(prices: np.ndarray, period: int) -> float: + """计算指数移动平均线""" + if len(prices) < period: + return prices[-1] if len(prices) > 0 else 0 + + ema = prices[0] + alpha = 2.0 / (period + 1) + + for price in prices[1:]: + ema = alpha * price + (1 - alpha) * ema + + return round(ema, 2) + + +def calculate_ma_system(prices: np.ndarray) -> Dict[str, Any]: + """ + 计算完整的均线系统 + + 返回: + MA5, MA10, MA20, MA60 及趋势判断 + """ + ma5 = calculate_ma(prices, 5) + ma10 = calculate_ma(prices, 10) + ma20 = calculate_ma(prices, 20) + ma60 = calculate_ma(prices, 60) if len(prices) >= 60 else None + + current_price = prices[-1] + + # 判断均线排列 + if ma5 > ma10 > ma20: + arrangement = "多头排列" + trend = "bullish" + elif ma5 < ma10 < ma20: + arrangement = "空头排列" + trend = "bearish" + else: + arrangement = "均线缠绕" + trend = "neutral" + + # 判断价格与均线关系 + above_ma = [] + below_ma = [] + + for name, ma in [('MA5', ma5), ('MA10', ma10), ('MA20', ma20)]: + if current_price > ma: + above_ma.append(name) + else: + below_ma.append(name) + + return { + 'ma5': ma5, + 'ma10': ma10, + 'ma20': ma20, + 'ma60': ma60, + 'arrangement': arrangement, + 'trend': trend, + 'price_above': above_ma, + 'price_below': below_ma, + 'source': 'Local Calculation' + } + + +# ============================================ +# 成交量分析 +# ============================================ + +def calculate_volume_analysis( + volume: np.ndarray, + close: np.ndarray, + period: int = 20 +) -> Dict[str, Any]: + """ + 成交量分析 + + 分析量价配合关系 + """ + if len(volume) < period: + return {'error': '数据不足'} + + # 平均成交量 + avg_volume = np.mean(volume[-period:]) + current_volume = volume[-1] + + # 量比 = 当前成交量 / 平均成交量 + volume_ratio = current_volume / avg_volume if avg_volume > 0 else 1 + + # 价格变动 + price_change = (close[-1] - close[-2]) / close[-2] * 100 if len(close) > 1 else 0 + + # 量价配合判断 + if volume_ratio > 1.5 and price_change > 1: + pattern = "放量上涨" + signal = "bullish" + explanation = "成交量放大配合价格上涨,买盘积极,上涨动能充足" + elif volume_ratio > 1.5 and price_change < -1: + pattern = "放量下跌" + signal = "bearish" + explanation = "成交量放大配合价格下跌,卖盘涌出,需要警惕" + elif volume_ratio < 0.7 and price_change > 1: + pattern = "缩量上涨" + signal = "neutral" + explanation = "上涨但成交量萎缩,上涨动能不足,可能是反弹" + elif volume_ratio < 0.7 and price_change < -1: + pattern = "缩量下跌" + signal = "neutral" + explanation = "下跌但成交量萎缩,卖压减轻,可能接近底部" + elif volume_ratio > 1.5: + pattern = "放量震荡" + signal = "neutral" + explanation = "成交量放大但价格变化不大,多空博弈激烈" + else: + pattern = "量价平稳" + signal = "neutral" + explanation = "成交量和价格都在正常范围波动" + + return { + 'current_volume': int(current_volume), + 'avg_volume': int(avg_volume), + 'volume_ratio': round(volume_ratio, 2), + 'pattern': pattern, + 'signal': signal, + 'explanation': explanation, + 'source': 'Local Calculation' + } + + +# ============================================ +# MACD 本地计算 +# ============================================ + +def calculate_macd( + prices: np.ndarray, + fast_period: int = 12, + slow_period: int = 26, + signal_period: int = 9 +) -> Dict[str, Any]: + """ + 计算 MACD 指标 + + 返回: + MACD 线、信号线、柱状图及信号判断 + """ + if len(prices) < slow_period + signal_period: + return {'error': '数据不足'} + + # 计算 EMA + def ema(data, period): + alpha = 2.0 / (period + 1) + result = [data[0]] + for i in range(1, len(data)): + result.append(alpha * data[i] + (1 - alpha) * result[-1]) + return np.array(result) + + ema_fast = ema(prices, fast_period) + ema_slow = ema(prices, slow_period) + + # MACD 线 + macd_line = ema_fast - ema_slow + + # 信号线 + signal_line = ema(macd_line, signal_period) + + # 柱状图 + histogram = macd_line - signal_line + + # 获取最新值 + latest_macd = float(macd_line[-1]) + latest_signal = float(signal_line[-1]) + latest_hist = float(histogram[-1]) + prev_hist = float(histogram[-2]) if len(histogram) > 1 else 0 + + # 判断信号 + if latest_hist > 0 and prev_hist <= 0: + signal = 'golden_cross' + interpretation = 'MACD 金叉 - 买入信号' + elif latest_hist < 0 and prev_hist >= 0: + signal = 'death_cross' + interpretation = 'MACD 死叉 - 卖出信号' + elif latest_hist > 0: + signal = 'bullish' + interpretation = 'MACD 多头趋势' + else: + signal = 'bearish' + interpretation = 'MACD 空头趋势' + + return { + 'macd_line': round(latest_macd, 4), + 'signal_line': round(latest_signal, 4), + 'histogram': round(latest_hist, 4), + 'prev_histogram': round(prev_hist, 4), + 'signal': signal, + 'interpretation': interpretation, + 'source': 'Local Calculation' + } + + +# ============================================ +# KDJ 随机指标 +# ============================================ + +def calculate_kdj( + high: np.ndarray, + low: np.ndarray, + close: np.ndarray, + n: int = 9, + m1: int = 3, + m2: int = 3 +) -> Dict[str, Any]: + """ + 计算 KDJ 随机指标 + + KDJ 是国内股民最常用的短线指标,用于判断超买超卖。 + + 参数: + high: 最高价数组 + low: 最低价数组 + close: 收盘价数组 + n: RSV 周期,默认 9 + m1: K 值平滑周期,默认 3 + m2: D 值平滑周期,默认 3 + + 返回: + K, D, J 值及信号判断 + """ + if len(close) < n: + return {'error': '数据不足'} + + # 计算 RSV (Raw Stochastic Value) + rsv_list = [] + for i in range(n - 1, len(close)): + period_high = np.max(high[i - n + 1:i + 1]) + period_low = np.min(low[i - n + 1:i + 1]) + + if period_high == period_low: + rsv = 50.0 + else: + rsv = (close[i] - period_low) / (period_high - period_low) * 100 + rsv_list.append(rsv) + + rsv_array = np.array(rsv_list) + + # 计算 K 值 (RSV 的 EMA) + k_values = [50.0] # 初始值 + for i in range(len(rsv_array)): + k = (2 * k_values[-1] + rsv_array[i]) / 3 # 平滑系数 1/m1 + k_values.append(k) + k_values = k_values[1:] # 移除初始值 + + # 计算 D 值 (K 的 EMA) + d_values = [50.0] + for i in range(len(k_values)): + d = (2 * d_values[-1] + k_values[i]) / 3 + d_values.append(d) + d_values = d_values[1:] + + # 计算 J 值 + j_values = [3 * k - 2 * d for k, d in zip(k_values, d_values)] + + # 获取最新值 + k = k_values[-1] + d = d_values[-1] + j = j_values[-1] + + # 获取前一日值(用于判断金叉死叉) + prev_k = k_values[-2] if len(k_values) > 1 else k + prev_d = d_values[-2] if len(d_values) > 1 else d + + # 判断信号 + if k > d and prev_k <= prev_d: + signal = 'golden_cross' + interpretation = 'KDJ 金叉 - 买入信号' + elif k < d and prev_k >= prev_d: + signal = 'death_cross' + interpretation = 'KDJ 死叉 - 卖出信号' + elif j > 100: + signal = 'overbought' + interpretation = f'KDJ 超买 (J={j:.1f}>100) - 短期可能回调' + elif j < 0: + signal = 'oversold' + interpretation = f'KDJ 超卖 (J={j:.1f}<0) - 短期可能反弹' + elif k > 80 and d > 80: + signal = 'high_zone' + interpretation = 'KDJ 高位钝化 - 注意回调风险' + elif k < 20 and d < 20: + signal = 'low_zone' + interpretation = 'KDJ 低位钝化 - 关注反弹机会' + else: + signal = 'neutral' + interpretation = 'KDJ 中性区间' + + return { + 'k': round(k, 2), + 'd': round(d, 2), + 'j': round(j, 2), + 'signal': signal, + 'interpretation': interpretation, + 'source': 'Local Calculation' + } + + +# ============================================ +# 背离检测 (Divergence Detection) +# ============================================ + +def detect_divergence( + prices: np.ndarray, + indicator_values: np.ndarray, + lookback: int = 20 +) -> Dict[str, Any]: + """ + 检测价格与指标之间的背离 + + 背离是强烈的反转信号: + - 顶背离:价格创新高,指标未创新高 → 看跌 + - 底背离:价格创新低,指标未创新低 → 看涨 + + 参数: + prices: 价格数组 + indicator_values: 指标值数组(如 RSI、MACD) + lookback: 回溯周期,默认 20 + + 返回: + 背离类型和强度 + """ + if len(prices) < lookback or len(indicator_values) < lookback: + return {'divergence': None, 'type': 'none'} + + # 取最近的数据 + recent_prices = prices[-lookback:] + recent_indicator = indicator_values[-lookback:] + + # 找局部高点和低点 + def find_peaks(arr, is_high=True): + peaks = [] + for i in range(2, len(arr) - 2): + if is_high: + if arr[i] > arr[i-1] and arr[i] > arr[i-2] and arr[i] > arr[i+1] and arr[i] > arr[i+2]: + peaks.append((i, arr[i])) + else: + if arr[i] < arr[i-1] and arr[i] < arr[i-2] and arr[i] < arr[i+1] and arr[i] < arr[i+2]: + peaks.append((i, arr[i])) + return peaks + + price_highs = find_peaks(recent_prices, is_high=True) + price_lows = find_peaks(recent_prices, is_high=False) + ind_highs = find_peaks(recent_indicator, is_high=True) + ind_lows = find_peaks(recent_indicator, is_high=False) + + divergence_type = 'none' + interpretation = '无明显背离' + strength = 0 + + # 检测顶背离 + if len(price_highs) >= 2 and len(ind_highs) >= 2: + # 价格新高创新高 + if price_highs[-1][1] > price_highs[-2][1]: + # 但指标未创新高 + if ind_highs[-1][1] < ind_highs[-2][1]: + divergence_type = 'bearish' + strength = abs(ind_highs[-2][1] - ind_highs[-1][1]) + interpretation = '顶背离 - 价格创新高但动能减弱,警惕回调' + + # 检测底背离 + if len(price_lows) >= 2 and len(ind_lows) >= 2: + # 价格创新低 + if price_lows[-1][1] < price_lows[-2][1]: + # 但指标未创新低 + if ind_lows[-1][1] > ind_lows[-2][1]: + divergence_type = 'bullish' + strength = abs(ind_lows[-1][1] - ind_lows[-2][1]) + interpretation = '底背离 - 价格创新低但动能增强,关注反弹' + + return { + 'divergence': divergence_type, + 'strength': round(strength, 2), + 'interpretation': interpretation, + 'source': 'Local Calculation' + } + + +def detect_macd_divergence( + prices: np.ndarray, + fast_period: int = 12, + slow_period: int = 26, + signal_period: int = 9, + lookback: int = 30 +) -> Dict[str, Any]: + """检测 MACD 背离""" + macd_result = calculate_macd(prices, fast_period, slow_period, signal_period) + if 'error' in macd_result: + return macd_result + + # 重新计算完整的 MACD 序列 + def ema(data, period): + alpha = 2.0 / (period + 1) + result = [data[0]] + for i in range(1, len(data)): + result.append(alpha * data[i] + (1 - alpha) * result[-1]) + return np.array(result) + + ema_fast = ema(prices, fast_period) + ema_slow = ema(prices, slow_period) + macd_line = ema_fast - ema_slow + + return detect_divergence(prices, macd_line, lookback) + + +def detect_rsi_divergence( + prices: np.ndarray, + period: int = 14, + lookback: int = 30 +) -> Dict[str, Any]: + """检测 RSI 背离""" + rsi_series = calculate_rsi_series(prices, period) + + if len(rsi_series) < lookback: + return {'divergence': None, 'type': 'none'} + + # 对齐价格和 RSI 序列 + aligned_prices = prices[-(len(rsi_series)):] + + return detect_divergence(aligned_prices, rsi_series, lookback) + + +# ============================================ +# 增强版支撑阻力位(含斐波那契) +# ============================================ + +def calculate_support_resistance_enhanced( + high: np.ndarray, + low: np.ndarray, + close: np.ndarray, + lookback: int = 60 +) -> Dict[str, Any]: + """ + 计算增强版支撑阻力位 + + 方法: + 1. 近期高低点 + 2. 斐波那契回撤位 + 3. 整数关口 + + 参数: + high: 最高价数组 + low: 最低价数组 + close: 收盘价数组 + lookback: 回溯周期 + + 返回: + 多级支撑位和阻力位 + """ + if len(close) < lookback: + lookback = len(close) + + recent_high = high[-lookback:] + recent_low = low[-lookback:] + current_price = close[-1] + + # 1. 近期高低点 + period_high = np.max(recent_high) + period_low = np.min(recent_low) + + # 2. 斐波那契回撤位 + diff = period_high - period_low + fib_levels = { + '0.0%': period_low, + '23.6%': period_low + diff * 0.236, + '38.2%': period_low + diff * 0.382, + '50.0%': period_low + diff * 0.500, + '61.8%': period_low + diff * 0.618, + '78.6%': period_low + diff * 0.786, + '100.0%': period_high, + } + + # 3. 扩展斐波那契(预测突破后目标) + fib_extensions = { + '127.2%': period_high + diff * 0.272, + '161.8%': period_high + diff * 0.618, + '200.0%': period_high + diff * 1.0, + } + + # 4. 整数关口(心理价位) + def find_round_numbers(price, range_pct=0.1): + """找到价格附近的整数关口""" + rounds = [] + base = 10 ** (len(str(int(price))) - 1) # 确定量级 + + for multiplier in [1, 5, 10, 50, 100]: + step = base / 10 * multiplier + lower = int(price * (1 - range_pct) / step) * step + upper = int(price * (1 + range_pct) / step) * step + step + + current = lower + while current <= upper: + if abs(current - price) / price < range_pct: + rounds.append(current) + current += step + + return sorted(set(rounds)) + + round_numbers = find_round_numbers(current_price) + + # 分类支撑位和阻力位 + supports = [] + resistances = [] + + # 添加斐波那契位 + for name, level in fib_levels.items(): + if level < current_price: + supports.append({'price': round(level, 2), 'type': f'Fib {name}', 'strength': 'medium'}) + elif level > current_price: + resistances.append({'price': round(level, 2), 'type': f'Fib {name}', 'strength': 'medium'}) + + # 添加扩展位作为阻力 + for name, level in fib_extensions.items(): + resistances.append({'price': round(level, 2), 'type': f'Fib Ext {name}', 'strength': 'weak'}) + + # 标记关键位 + for s in supports: + if s['type'] in ['Fib 38.2%', 'Fib 50.0%', 'Fib 61.8%']: + s['strength'] = 'strong' + + for r in resistances: + if r['type'] in ['Fib 38.2%', 'Fib 50.0%', 'Fib 61.8%']: + r['strength'] = 'strong' + + # 排序 + supports = sorted(supports, key=lambda x: x['price'], reverse=True)[:5] + resistances = sorted(resistances, key=lambda x: x['price'])[:5] + + # 找最近的支撑和阻力 + nearest_support = supports[0] if supports else None + nearest_resistance = resistances[0] if resistances else None + + return { + 'current_price': round(current_price, 2), + 'period_high': round(period_high, 2), + 'period_low': round(period_low, 2), + 'supports': supports, + 'resistances': resistances, + 'nearest_support': nearest_support, + 'nearest_resistance': nearest_resistance, + 'fibonacci': fib_levels, + 'round_numbers': round_numbers[:5], + 'source': 'Local Calculation' + } + + +# ============================================ +# OBV 量能指标 +# ============================================ + +def calculate_obv( + close: np.ndarray, + volume: np.ndarray +) -> Dict[str, Any]: + """ + 计算 OBV (On-Balance Volume) 能量潮 + + 原理: + - 上涨日:OBV += 成交量 + - 下跌日:OBV -= 成交量 + - 平盘日:OBV 不变 + + 用途: + - OBV 上升 + 价格上升 = 趋势确认 + - OBV 上升 + 价格下跌 = 底背离,可能反转向上 + - OBV 下降 + 价格上升 = 顶背离,可能反转向下 + """ + if len(close) < 2 or len(volume) < 2: + return {'error': '数据不足'} + + obv = [0] + for i in range(1, len(close)): + if close[i] > close[i-1]: + obv.append(obv[-1] + volume[i]) + elif close[i] < close[i-1]: + obv.append(obv[-1] - volume[i]) + else: + obv.append(obv[-1]) + + obv = np.array(obv) + + # 计算 OBV 的移动平均 + obv_ma = np.mean(obv[-20:]) if len(obv) >= 20 else np.mean(obv) + + # 判断趋势 + recent_obv = obv[-5:] + obv_trend = 'up' if recent_obv[-1] > recent_obv[0] else 'down' + + recent_price = close[-5:] + price_trend = 'up' if recent_price[-1] > recent_price[0] else 'down' + + # 判断信号 + if obv_trend == 'up' and price_trend == 'up': + signal = 'confirmed_up' + interpretation = 'OBV 确认上涨 - 量价配合,趋势健康' + elif obv_trend == 'down' and price_trend == 'down': + signal = 'confirmed_down' + interpretation = 'OBV 确认下跌 - 量价配合,趋势延续' + elif obv_trend == 'up' and price_trend == 'down': + signal = 'bullish_divergence' + interpretation = 'OBV 底背离 - 资金悄悄流入,关注反弹' + elif obv_trend == 'down' and price_trend == 'up': + signal = 'bearish_divergence' + interpretation = 'OBV 顶背离 - 资金悄悄流出,警惕回调' + else: + signal = 'neutral' + interpretation = 'OBV 中性' + + return { + 'obv': int(obv[-1]), + 'obv_ma': int(obv_ma), + 'obv_trend': obv_trend, + 'price_trend': price_trend, + 'signal': signal, + 'interpretation': interpretation, + 'source': 'Local Calculation' + } + + +# ============================================ +# 威廉指标 Williams %R +# ============================================ + +def calculate_williams_r( + high: np.ndarray, + low: np.ndarray, + close: np.ndarray, + period: int = 14 +) -> Dict[str, Any]: + """ + 计算威廉指标 Williams %R + + 与 KDJ 类似,但取值范围 0 到 -100: + - 0 到 -20: 超买区 + - -80 到 -100: 超卖区 + """ + if len(close) < period: + return {'error': '数据不足'} + + highest_high = np.max(high[-period:]) + lowest_low = np.min(low[-period:]) + current_close = close[-1] + + if highest_high == lowest_low: + wr = -50.0 + else: + wr = (highest_high - current_close) / (highest_high - lowest_low) * -100 + + # 判断信号 + if wr > -20: + signal = 'overbought' + interpretation = f'威廉指标超买 ({wr:.1f}) - 短期可能回调' + elif wr < -80: + signal = 'oversold' + interpretation = f'威廉指标超卖 ({wr:.1f}) - 短期可能反弹' + else: + signal = 'neutral' + interpretation = f'威廉指标中性 ({wr:.1f})' + + return { + 'williams_r': round(wr, 2), + 'signal': signal, + 'interpretation': interpretation, + 'source': 'Local Calculation' + } + + +# ============================================ +# 乖离率 BIAS +# ============================================ + +def calculate_bias( + close: np.ndarray, + periods: List[int] = [6, 12, 24] +) -> Dict[str, Any]: + """ + 计算乖离率 BIAS + + 乖离率 = (当前价 - MA) / MA × 100% + + 用途: + - 正乖离过大:股价高于均线太多,可能回调 + - 负乖离过大:股价低于均线太多,可能反弹 + """ + result = {} + signals = [] + + for period in periods: + if len(close) < period: + continue + + ma = np.mean(close[-period:]) + bias = (close[-1] - ma) / ma * 100 + + result[f'bias{period}'] = round(bias, 2) + result[f'ma{period}'] = round(ma, 2) + + # 判断信号(一般以 6% 为界) + if bias > 6: + signals.append(f'BIAS{period} 正乖离过大 ({bias:.1f}%)') + elif bias < -6: + signals.append(f'BIAS{period} 负乖离过大 ({bias:.1f}%)') + + # 综合判断 + if any('正乖离过大' in s for s in signals): + interpretation = '乖离率偏高 - 股价偏离均线过远,注意回调' + signal = 'overbought' + elif any('负乖离过大' in s for s in signals): + interpretation = '乖离率偏低 - 股价偏离均线过远,关注反弹' + signal = 'oversold' + else: + interpretation = '乖离率正常' + signal = 'neutral' + + result['signal'] = signal + result['interpretation'] = interpretation + result['source'] = 'Local Calculation' + + return result + + +# ============================================ +# K线形态识别 (Candlestick Patterns) +# ============================================ + +def identify_candlestick_patterns( + open_prices: np.ndarray, + high: np.ndarray, + low: np.ndarray, + close: np.ndarray +) -> Dict[str, Any]: + """ + 识别K线形态 + + 支持的形态: + - 锤子线/吊颈线 (Hammer/Hanging Man) + - 倒锤子/射击之星 (Inverted Hammer/Shooting Star) + - 吞没形态 (Engulfing) + - 十字星 (Doji) + - 早晨之星/黄昏之星 (Morning Star/Evening Star) + - 三只乌鸦/三白兵 (Three Crows/Three White Soldiers) + """ + if len(close) < 5: + return {'patterns': [], 'signal': 'neutral'} + + patterns = [] + + # 计算K线实体和影线 + def candle_info(i): + body = close[i] - open_prices[i] + body_size = abs(body) + upper_shadow = high[i] - max(open_prices[i], close[i]) + lower_shadow = min(open_prices[i], close[i]) - low[i] + total_range = high[i] - low[i] + is_bullish = close[i] > open_prices[i] + return { + 'body': body, + 'body_size': body_size, + 'upper_shadow': upper_shadow, + 'lower_shadow': lower_shadow, + 'total_range': total_range, + 'is_bullish': is_bullish, + 'body_ratio': body_size / total_range if total_range > 0 else 0 + } + + # 获取最近几根K线信息 + c0 = candle_info(-1) # 最新 + c1 = candle_info(-2) # 前一根 + c2 = candle_info(-3) if len(close) >= 3 else None + + # 判断近期趋势 + recent_trend = 'up' if close[-1] > close[-5] else 'down' + avg_body = np.mean([abs(close[i] - open_prices[i]) for i in range(-10, -1)]) + + # === 1. 十字星 (Doji) === + if c0['body_ratio'] < 0.1 and c0['total_range'] > 0: + if recent_trend == 'up': + patterns.append({ + 'name': '十字星', + 'name_en': 'Doji', + 'type': 'reversal', + 'signal': 'bearish', + 'strength': 'medium', + 'description': '多空平衡,上涨趋势可能转折' + }) + else: + patterns.append({ + 'name': '十字星', + 'name_en': 'Doji', + 'type': 'reversal', + 'signal': 'bullish', + 'strength': 'medium', + 'description': '多空平衡,下跌趋势可能转折' + }) + + # === 2. 锤子线/吊颈线 (Hammer/Hanging Man) === + # 特征:小实体在上方,长下影线(>=实体2倍),几乎无上影线 + if (c0['lower_shadow'] >= c0['body_size'] * 2 and + c0['upper_shadow'] < c0['body_size'] * 0.3 and + c0['body_size'] > avg_body * 0.3): + if recent_trend == 'down': + patterns.append({ + 'name': '锤子线', + 'name_en': 'Hammer', + 'type': 'reversal', + 'signal': 'bullish', + 'strength': 'strong', + 'description': '下跌中出现,下影线表示多方抵抗,可能反弹' + }) + else: + patterns.append({ + 'name': '吊颈线', + 'name_en': 'Hanging Man', + 'type': 'reversal', + 'signal': 'bearish', + 'strength': 'medium', + 'description': '上涨中出现,警示上涨动能减弱' + }) + + # === 3. 倒锤子/射击之星 (Inverted Hammer/Shooting Star) === + # 特征:小实体在下方,长上影线,几乎无下影线 + if (c0['upper_shadow'] >= c0['body_size'] * 2 and + c0['lower_shadow'] < c0['body_size'] * 0.3 and + c0['body_size'] > avg_body * 0.3): + if recent_trend == 'down': + patterns.append({ + 'name': '倒锤子', + 'name_en': 'Inverted Hammer', + 'type': 'reversal', + 'signal': 'bullish', + 'strength': 'medium', + 'description': '下跌中出现,上影线表示买方尝试,可能反弹' + }) + else: + patterns.append({ + 'name': '射击之星', + 'name_en': 'Shooting Star', + 'type': 'reversal', + 'signal': 'bearish', + 'strength': 'strong', + 'description': '上涨中出现,上影线表示卖压,可能回调' + }) + + # === 4. 看涨吞没 (Bullish Engulfing) === + if (not c1['is_bullish'] and c0['is_bullish'] and + c0['body_size'] > c1['body_size'] * 1.2 and + open_prices[-1] < close[-2] and close[-1] > open_prices[-2]): + patterns.append({ + 'name': '看涨吞没', + 'name_en': 'Bullish Engulfing', + 'type': 'reversal', + 'signal': 'bullish', + 'strength': 'strong', + 'description': '阳线完全吞没前一根阴线,强烈反弹信号' + }) + + # === 5. 看跌吞没 (Bearish Engulfing) === + if (c1['is_bullish'] and not c0['is_bullish'] and + c0['body_size'] > c1['body_size'] * 1.2 and + open_prices[-1] > close[-2] and close[-1] < open_prices[-2]): + patterns.append({ + 'name': '看跌吞没', + 'name_en': 'Bearish Engulfing', + 'type': 'reversal', + 'signal': 'bearish', + 'strength': 'strong', + 'description': '阴线完全吞没前一根阳线,强烈回调信号' + }) + + # === 6. 早晨之星 (Morning Star) === + if c2 and len(close) >= 3: + # 第一根大阴线,第二根小实体(星),第三根大阳线 + if (not candle_info(-3)['is_bullish'] and + candle_info(-3)['body_size'] > avg_body * 1.2 and + candle_info(-2)['body_size'] < avg_body * 0.5 and + c0['is_bullish'] and + c0['body_size'] > avg_body * 1.2 and + close[-1] > (open_prices[-3] + close[-3]) / 2): + patterns.append({ + 'name': '早晨之星', + 'name_en': 'Morning Star', + 'type': 'reversal', + 'signal': 'bullish', + 'strength': 'very_strong', + 'description': '经典底部反转形态,强烈买入信号' + }) + + # === 7. 黄昏之星 (Evening Star) === + if c2 and len(close) >= 3: + if (candle_info(-3)['is_bullish'] and + candle_info(-3)['body_size'] > avg_body * 1.2 and + candle_info(-2)['body_size'] < avg_body * 0.5 and + not c0['is_bullish'] and + c0['body_size'] > avg_body * 1.2 and + close[-1] < (open_prices[-3] + close[-3]) / 2): + patterns.append({ + 'name': '黄昏之星', + 'name_en': 'Evening Star', + 'type': 'reversal', + 'signal': 'bearish', + 'strength': 'very_strong', + 'description': '经典顶部反转形态,强烈卖出信号' + }) + + # === 8. 三白兵 (Three White Soldiers) === + if len(close) >= 3: + three_bullish = all(close[-i] > open_prices[-i] for i in range(1, 4)) + ascending = close[-1] > close[-2] > close[-3] + good_body = all(abs(close[-i] - open_prices[-i]) > avg_body * 0.5 for i in range(1, 4)) + + if three_bullish and ascending and good_body: + patterns.append({ + 'name': '三白兵', + 'name_en': 'Three White Soldiers', + 'type': 'continuation', + 'signal': 'bullish', + 'strength': 'very_strong', + 'description': '连续三根上涨阳线,强势上涨信号' + }) + + # === 9. 三只乌鸦 (Three Black Crows) === + if len(close) >= 3: + three_bearish = all(close[-i] < open_prices[-i] for i in range(1, 4)) + descending = close[-1] < close[-2] < close[-3] + good_body = all(abs(close[-i] - open_prices[-i]) > avg_body * 0.5 for i in range(1, 4)) + + if three_bearish and descending and good_body: + patterns.append({ + 'name': '三只乌鸦', + 'name_en': 'Three Black Crows', + 'type': 'continuation', + 'signal': 'bearish', + 'strength': 'very_strong', + 'description': '连续三根下跌阴线,强势下跌信号' + }) + + # 确定综合信号 + bullish_count = sum(1 for p in patterns if p['signal'] == 'bullish') + bearish_count = sum(1 for p in patterns if p['signal'] == 'bearish') + + if bullish_count > bearish_count: + overall_signal = 'bullish' + elif bearish_count > bullish_count: + overall_signal = 'bearish' + else: + overall_signal = 'neutral' + + return { + 'patterns': patterns, + 'signal': overall_signal, + 'bullish_count': bullish_count, + 'bearish_count': bearish_count, + 'source': 'Local Calculation' + } + + +# ============================================ +# 趋势形态识别 (Chart Patterns) +# ============================================ + +def identify_chart_patterns( + high: np.ndarray, + low: np.ndarray, + close: np.ndarray, + lookback: int = 60 +) -> Dict[str, Any]: + """ + 识别趋势形态 + + 支持的形态: + - 双底 (Double Bottom) / W底 + - 双顶 (Double Top) / M顶 + - 头肩顶 (Head and Shoulders) + - 头肩底 (Inverse Head and Shoulders) + - 上升三角形 (Ascending Triangle) + - 下降三角形 (Descending Triangle) + - 对称三角形 (Symmetric Triangle) + """ + if len(close) < lookback: + lookback = len(close) + + if lookback < 20: + return {'patterns': [], 'signal': 'neutral'} + + patterns = [] + recent_close = close[-lookback:] + recent_high = high[-lookback:] + recent_low = low[-lookback:] + + # 找局部极值点 + def find_local_extrema(data, order=5): + """找局部高点和低点""" + highs = [] + lows = [] + for i in range(order, len(data) - order): + if all(data[i] >= data[i-j] for j in range(1, order+1)) and \ + all(data[i] >= data[i+j] for j in range(1, order+1)): + highs.append((i, data[i])) + if all(data[i] <= data[i-j] for j in range(1, order+1)) and \ + all(data[i] <= data[i+j] for j in range(1, order+1)): + lows.append((i, data[i])) + return highs, lows + + local_highs, local_lows = find_local_extrema(recent_close, order=3) + + # === 1. 双底 (Double Bottom / W底) === + # 特征:两个相近的低点,中间有反弹 + if len(local_lows) >= 2: + for i in range(len(local_lows) - 1): + low1_idx, low1_val = local_lows[i] + low2_idx, low2_val = local_lows[i + 1] + + # 两个低点间隔要够远 + if low2_idx - low1_idx < 10: + continue + + # 两个低点价格相近(差距<3%) + diff_pct = abs(low1_val - low2_val) / low1_val * 100 + if diff_pct > 3: + continue + + # 中间有明显反弹 + middle_high = max(recent_close[low1_idx:low2_idx]) + rebound_pct = (middle_high - low1_val) / low1_val * 100 + + if rebound_pct > 5: + # 判断是否已突破颈线 + neckline = middle_high + current_price = close[-1] + + if current_price > neckline: + patterns.append({ + 'name': '双底突破', + 'name_en': 'Double Bottom Breakout', + 'type': 'reversal', + 'signal': 'bullish', + 'strength': 'very_strong', + 'neckline': round(neckline, 2), + 'target': round(neckline + (neckline - low1_val), 2), + 'description': f'W底形态已突破颈线 ${neckline:.2f},目标位 ${neckline + (neckline - low1_val):.2f}' + }) + else: + patterns.append({ + 'name': '双底形成中', + 'name_en': 'Double Bottom Forming', + 'type': 'reversal', + 'signal': 'bullish', + 'strength': 'medium', + 'neckline': round(neckline, 2), + 'description': f'W底形态形成中,关注颈线 ${neckline:.2f} 突破' + }) + break + + # === 2. 双顶 (Double Top / M顶) === + if len(local_highs) >= 2: + for i in range(len(local_highs) - 1): + high1_idx, high1_val = local_highs[i] + high2_idx, high2_val = local_highs[i + 1] + + if high2_idx - high1_idx < 10: + continue + + diff_pct = abs(high1_val - high2_val) / high1_val * 100 + if diff_pct > 3: + continue + + middle_low = min(recent_close[high1_idx:high2_idx]) + pullback_pct = (high1_val - middle_low) / high1_val * 100 + + if pullback_pct > 5: + neckline = middle_low + current_price = close[-1] + + if current_price < neckline: + patterns.append({ + 'name': '双顶跌破', + 'name_en': 'Double Top Breakdown', + 'type': 'reversal', + 'signal': 'bearish', + 'strength': 'very_strong', + 'neckline': round(neckline, 2), + 'target': round(neckline - (high1_val - neckline), 2), + 'description': f'M顶形态已跌破颈线 ${neckline:.2f},目标位 ${neckline - (high1_val - neckline):.2f}' + }) + else: + patterns.append({ + 'name': '双顶形成中', + 'name_en': 'Double Top Forming', + 'type': 'reversal', + 'signal': 'bearish', + 'strength': 'medium', + 'neckline': round(neckline, 2), + 'description': f'M顶形态形成中,关注颈线 ${neckline:.2f} 跌破' + }) + break + + # === 3. 头肩顶 (Head and Shoulders) === + if len(local_highs) >= 3: + for i in range(len(local_highs) - 2): + left_idx, left_val = local_highs[i] + head_idx, head_val = local_highs[i + 1] + right_idx, right_val = local_highs[i + 2] + + # 头部最高,两肩相近 + if head_val > left_val and head_val > right_val: + shoulder_diff = abs(left_val - right_val) / left_val * 100 + if shoulder_diff < 5: + # 找颈线(两肩之间的低点) + neckline_left = min(recent_close[left_idx:head_idx]) + neckline_right = min(recent_close[head_idx:right_idx]) + neckline = (neckline_left + neckline_right) / 2 + + current_price = close[-1] + if current_price < neckline: + patterns.append({ + 'name': '头肩顶跌破', + 'name_en': 'Head and Shoulders Breakdown', + 'type': 'reversal', + 'signal': 'bearish', + 'strength': 'very_strong', + 'neckline': round(neckline, 2), + 'target': round(neckline - (head_val - neckline), 2), + 'description': f'头肩顶已跌破颈线,目标位 ${neckline - (head_val - neckline):.2f}' + }) + else: + patterns.append({ + 'name': '头肩顶形成中', + 'name_en': 'Head and Shoulders Forming', + 'type': 'reversal', + 'signal': 'bearish', + 'strength': 'strong', + 'neckline': round(neckline, 2), + 'description': f'头肩顶形成中,关注颈线 ${neckline:.2f}' + }) + break + + # === 4. 头肩底 (Inverse Head and Shoulders) === + if len(local_lows) >= 3: + for i in range(len(local_lows) - 2): + left_idx, left_val = local_lows[i] + head_idx, head_val = local_lows[i + 1] + right_idx, right_val = local_lows[i + 2] + + if head_val < left_val and head_val < right_val: + shoulder_diff = abs(left_val - right_val) / left_val * 100 + if shoulder_diff < 5: + neckline_left = max(recent_close[left_idx:head_idx]) + neckline_right = max(recent_close[head_idx:right_idx]) + neckline = (neckline_left + neckline_right) / 2 + + current_price = close[-1] + if current_price > neckline: + patterns.append({ + 'name': '头肩底突破', + 'name_en': 'Inverse Head and Shoulders Breakout', + 'type': 'reversal', + 'signal': 'bullish', + 'strength': 'very_strong', + 'neckline': round(neckline, 2), + 'target': round(neckline + (neckline - head_val), 2), + 'description': f'头肩底已突破颈线,目标位 ${neckline + (neckline - head_val):.2f}' + }) + else: + patterns.append({ + 'name': '头肩底形成中', + 'name_en': 'Inverse Head and Shoulders Forming', + 'type': 'reversal', + 'signal': 'bullish', + 'strength': 'strong', + 'neckline': round(neckline, 2), + 'description': f'头肩底形成中,关注颈线 ${neckline:.2f}' + }) + break + + # === 5. 三角形整理 === + # 使用线性回归判断高点和低点趋势 + if len(local_highs) >= 3 and len(local_lows) >= 3: + high_indices = [h[0] for h in local_highs[-5:]] + high_values = [h[1] for h in local_highs[-5:]] + low_indices = [l[0] for l in local_lows[-5:]] + low_values = [l[1] for l in local_lows[-5:]] + + if len(high_indices) >= 2 and len(low_indices) >= 2: + # 简单判断趋势 + high_trend = (high_values[-1] - high_values[0]) / (high_indices[-1] - high_indices[0] + 1) + low_trend = (low_values[-1] - low_values[0]) / (low_indices[-1] - low_indices[0] + 1) + + # 上升三角形:高点水平,低点上升 + if abs(high_trend) < 0.01 * np.mean(high_values) and low_trend > 0.01 * np.mean(low_values): + resistance = np.mean(high_values) + if close[-1] > resistance: + patterns.append({ + 'name': '上升三角形突破', + 'name_en': 'Ascending Triangle Breakout', + 'type': 'continuation', + 'signal': 'bullish', + 'strength': 'strong', + 'resistance': round(resistance, 2), + 'description': f'上升三角形已突破阻力位 ${resistance:.2f}' + }) + else: + patterns.append({ + 'name': '上升三角形', + 'name_en': 'Ascending Triangle', + 'type': 'continuation', + 'signal': 'bullish', + 'strength': 'medium', + 'resistance': round(resistance, 2), + 'description': f'上升三角形整理中,关注 ${resistance:.2f} 突破' + }) + + # 下降三角形:低点水平,高点下降 + elif abs(low_trend) < 0.01 * np.mean(low_values) and high_trend < -0.01 * np.mean(high_values): + support = np.mean(low_values) + if close[-1] < support: + patterns.append({ + 'name': '下降三角形跌破', + 'name_en': 'Descending Triangle Breakdown', + 'type': 'continuation', + 'signal': 'bearish', + 'strength': 'strong', + 'support': round(support, 2), + 'description': f'下降三角形已跌破支撑位 ${support:.2f}' + }) + else: + patterns.append({ + 'name': '下降三角形', + 'name_en': 'Descending Triangle', + 'type': 'continuation', + 'signal': 'bearish', + 'strength': 'medium', + 'support': round(support, 2), + 'description': f'下降三角形整理中,关注 ${support:.2f} 跌破' + }) + + # 对称三角形 + elif high_trend < 0 and low_trend > 0: + patterns.append({ + 'name': '对称三角形', + 'name_en': 'Symmetric Triangle', + 'type': 'continuation', + 'signal': 'neutral', + 'strength': 'medium', + 'description': '对称三角形收敛中,等待方向选择' + }) + + # 确定综合信号 + bullish_count = sum(1 for p in patterns if p['signal'] == 'bullish') + bearish_count = sum(1 for p in patterns if p['signal'] == 'bearish') + + # 考虑强度权重 + bullish_strength = sum( + 3 if p['strength'] == 'very_strong' else 2 if p['strength'] == 'strong' else 1 + for p in patterns if p['signal'] == 'bullish' + ) + bearish_strength = sum( + 3 if p['strength'] == 'very_strong' else 2 if p['strength'] == 'strong' else 1 + for p in patterns if p['signal'] == 'bearish' + ) + + if bullish_strength > bearish_strength: + overall_signal = 'bullish' + elif bearish_strength > bullish_strength: + overall_signal = 'bearish' + else: + overall_signal = 'neutral' + + return { + 'patterns': patterns, + 'signal': overall_signal, + 'bullish_count': bullish_count, + 'bearish_count': bearish_count, + 'source': 'Local Calculation' + } + + +def analyze_patterns( + open_prices: np.ndarray, + high: np.ndarray, + low: np.ndarray, + close: np.ndarray +) -> Dict[str, Any]: + """ + 综合分析所有形态(K线 + 趋势) + """ + candlestick = identify_candlestick_patterns(open_prices, high, low, close) + chart = identify_chart_patterns(high, low, close) + + all_patterns = candlestick['patterns'] + chart['patterns'] + + # 按强度排序 + strength_order = {'very_strong': 0, 'strong': 1, 'medium': 2, 'weak': 3} + all_patterns.sort(key=lambda x: strength_order.get(x.get('strength', 'weak'), 3)) + + # 综合判断 + bullish = sum(1 for p in all_patterns if p['signal'] == 'bullish') + bearish = sum(1 for p in all_patterns if p['signal'] == 'bearish') + + if bullish > bearish: + overall = 'bullish' + elif bearish > bullish: + overall = 'bearish' + else: + overall = 'neutral' + + return { + 'candlestick_patterns': candlestick['patterns'], + 'chart_patterns': chart['patterns'], + 'all_patterns': all_patterns, + 'signal': overall, + 'bullish_count': bullish, + 'bearish_count': bearish, + 'source': 'Local Calculation' + } + + +# ============================================ +# 动态止损计算 +# ============================================ + +def calculate_dynamic_stop_loss( + current_price: float, + atr: float, + atr_multiplier: float = 2.0, + action: str = 'BUY' +) -> Dict[str, float]: + """ + 基于 ATR 计算动态止损 + + 参数: + current_price: 当前价格 + atr: ATR 值 + atr_multiplier: ATR 倍数(波动大的股票用更大倍数) + action: 操作方向 (BUY/SELL) + + 返回: + 止损价、止盈价、风险收益比 + """ + if action == 'BUY': + # 买入:止损在下方 + stop_loss = current_price - atr * atr_multiplier + # 止盈通常是止损距离的 2-3 倍 + take_profit = current_price + atr * atr_multiplier * 2.5 + else: + # 卖出:止损在上方 + stop_loss = current_price + atr * atr_multiplier + take_profit = current_price - atr * atr_multiplier * 2.5 + + # 计算风险收益比 + risk = abs(current_price - stop_loss) + reward = abs(take_profit - current_price) + risk_reward_ratio = reward / risk if risk > 0 else 0 + + return { + 'stop_loss': round(stop_loss, 2), + 'take_profit': round(take_profit, 2), + 'atr': round(atr, 4), + 'atr_multiplier': atr_multiplier, + 'risk_amount': round(risk, 2), + 'reward_amount': round(reward, 2), + 'risk_reward_ratio': round(risk_reward_ratio, 2) + } + + +# ============================================ +# 仓位建议计算 +# ============================================ + +def calculate_position_size( + total_capital: float, + current_price: float, + stop_loss: float, + risk_percent: float = 2.0 +) -> Dict[str, Any]: + """ + 计算建议仓位大小 + + 基于固定风险百分比法则: + - 每笔交易最多亏损账户总额的 risk_percent% + + 参数: + total_capital: 账户总资金 + current_price: 当前价格 + stop_loss: 止损价 + risk_percent: 单笔最大风险百分比,默认 2% + + 返回: + 建议买入股数、仓位占比、最大亏损金额 + """ + # 单笔最大风险金额 + max_risk_amount = total_capital * (risk_percent / 100) + + # 每股风险 = 买入价 - 止损价 + risk_per_share = abs(current_price - stop_loss) + + if risk_per_share == 0: + return {'error': '止损价不能等于买入价'} + + # 建议股数 = 最大风险金额 / 每股风险 + suggested_shares = int(max_risk_amount / risk_per_share) + + # 需要资金 + required_capital = suggested_shares * current_price + + # 仓位占比 + position_percent = (required_capital / total_capital) * 100 + + return { + 'suggested_shares': suggested_shares, + 'required_capital': round(required_capital, 2), + 'position_percent': round(position_percent, 2), + 'max_loss_amount': round(max_risk_amount, 2), + 'risk_per_share': round(risk_per_share, 2), + 'risk_percent': risk_percent + } + + +# ============================================ +# 综合分析函数 +# ============================================ + +def analyze_stock_local(ticker: str, period: str = '3mo') -> Dict[str, Any]: + """ + 使用本地计算进行完整的股票分析 (v3.4) + + 适用于: + - 港股(Alpha Vantage 不支持) + - AV API 限流时的备选方案 + + 返回: + 包含所有指标的完整分析结果 + """ + # 获取数据 + data = get_stock_data(ticker, period) + + if 'error' in data: + return data + + close = data['close'] + high = data['high'] + low = data['low'] + open_prices = data['open'] + volume = data['volume'] + current_price = data['current_price'] + + # === 基础指标 === + rsi = calculate_rsi(close) + bbands = calculate_bollinger_bands(close) + macd = calculate_macd(close) + atr = calculate_atr(high, low, close) + atr_pct = calculate_atr_percent(high, low, close) + ma_system = calculate_ma_system(close) + volume_analysis = calculate_volume_analysis(volume, close) + + # === v3.3 新增指标 === + kdj = calculate_kdj(high, low, close) + obv = calculate_obv(close, volume) + williams = calculate_williams_r(high, low, close) + bias = calculate_bias(close) + support_resistance = calculate_support_resistance_enhanced(high, low, close) + + # === 背离检测 === + macd_divergence = detect_macd_divergence(close) + rsi_divergence = detect_rsi_divergence(close) + + # === v3.4 形态识别 === + patterns = analyze_patterns(open_prices, high, low, close) + + # 动态止损 + stop_loss_data = calculate_dynamic_stop_loss(current_price, atr, action='BUY') + + return { + 'ticker': ticker.upper(), + 'current_price': current_price, + 'timestamp': datetime.now().isoformat(), + 'source': 'Local Calculation (Yahoo Finance)', + 'indicators': { + # 基础指标 + 'rsi': rsi, + 'bbands': bbands, + 'macd': macd, + 'atr': atr, + 'atr_percent': atr_pct, + 'ma_system': ma_system, + 'volume': volume_analysis, + # v3.3 新增 + 'kdj': kdj, + 'obv': obv, + 'williams_r': williams, + 'bias': bias, + }, + 'support_resistance': support_resistance, + 'divergence': { + 'macd': macd_divergence, + 'rsi': rsi_divergence + }, + 'patterns': patterns, # v3.4 形态识别 + 'stop_loss': stop_loss_data, + 'prices': { + 'close_1m': close[-20:].tolist() if len(close) >= 20 else close.tolist(), + 'close_3m': close.tolist() + } + } + + +if __name__ == "__main__": + # 测试 + print("测试本地指标计算...") + + # 测试港股 + result = analyze_stock_local("1810.HK") + if 'error' not in result: + print(f"\n小米 (1810.HK) 分析结果:") + print(f" 当前价格: HK${result['current_price']:.2f}") + print(f" RSI: {result['indicators']['rsi']}") + print(f" 布林带: {result['indicators']['bbands']}") + print(f" MACD: {result['indicators']['macd']['interpretation']}") + print(f" ATR: {result['indicators']['atr']} ({result['indicators']['atr_percent']}%)") + print(f" 均线排列: {result['indicators']['ma_system']['arrangement']}") + print(f" 成交量: {result['indicators']['volume']['pattern']}") + print(f" 动态止损: ${result['stop_loss']['stop_loss']}") + print(f" 动态止盈: ${result['stop_loss']['take_profit']}") + else: + print(f" 错误: {result['error']}") diff --git a/skills/stock-master/scripts/main.py b/skills/stock-master/scripts/main.py new file mode 100644 index 00000000..8e653db1 --- /dev/null +++ b/skills/stock-master/scripts/main.py @@ -0,0 +1,827 @@ +""" +股票分析器 Skill - 主协调器 + +混合数据源架构: +- Yahoo Finance: 实时价格数据 + MACD 计算 +- Alpha Vantage MCP: RSI、Bollinger Bands 等技术指标 + +使用示例: + analyzer = StockAnalyzer() + result = analyzer.analyze("AAPL", ["RSI", "MACD", "Bollinger"]) + print(result) +""" + +from typing import List, Dict, Optional, Any, Tuple +from datetime import datetime, timedelta +from abc import ABC, abstractmethod +import json + + +class DataSource(ABC): + """数据源抽象基类""" + + @abstractmethod + def get_quote(self, ticker: str) -> Dict[str, Any]: + """获取最新报价""" + pass + + @abstractmethod + def get_indicator(self, ticker: str, indicator: str, **params) -> Dict[str, Any]: + """获取技术指标""" + pass + + +class YahooFinanceSource(DataSource): + """ + Yahoo Finance 数据源 + + 优势: + - 接近实时的价格数据 + - 可自行计算 MACD(Alpha Vantage 免费版不支持) + - 无严格请求限制 + """ + + def __init__(self): + self.name = "Yahoo Finance" + self._yf = None + self._ta = None + + def _ensure_imports(self): + """延迟导入依赖库""" + if self._yf is None: + try: + import yfinance as yf + self._yf = yf + except ImportError: + raise ImportError("请安装 yfinance: pip install yfinance") + + if self._ta is None: + try: + import pandas_ta as ta + self._ta = ta + except ImportError: + # pandas_ta 可选,MACD 可以手动计算 + self._ta = None + + def get_quote(self, ticker: str) -> Dict[str, Any]: + """获取 Yahoo Finance 实时报价""" + self._ensure_imports() + + stock = self._yf.Ticker(ticker) + info = stock.fast_info + + return { + 'source': self.name, + 'ticker': ticker.upper(), + 'price': float(info.last_price) if hasattr(info, 'last_price') else None, + 'open': float(info.open) if hasattr(info, 'open') else None, + 'high': float(info.day_high) if hasattr(info, 'day_high') else None, + 'low': float(info.day_low) if hasattr(info, 'day_low') else None, + 'volume': int(info.last_volume) if hasattr(info, 'last_volume') else None, + 'previous_close': float(info.previous_close) if hasattr(info, 'previous_close') else None, + 'timestamp': datetime.now().isoformat(), + 'realtime': True # Yahoo 数据接近实时 + } + + def get_indicator(self, ticker: str, indicator: str, **params) -> Dict[str, Any]: + """计算技术指标(使用 pandas_ta 或手动计算)""" + self._ensure_imports() + + # 获取历史数据 + stock = self._yf.Ticker(ticker) + period = params.get('period', '3mo') + hist = stock.history(period=period) + + if hist.empty: + return {'error': f'无法获取 {ticker} 的历史数据'} + + if indicator.upper() == 'MACD': + return self._calculate_macd(hist, params) + elif indicator.upper() == 'SMA': + return self._calculate_sma(hist, params) + elif indicator.upper() == 'EMA': + return self._calculate_ema(hist, params) + else: + return {'error': f'Yahoo 数据源不支持指标: {indicator}'} + + def _calculate_macd(self, hist, params) -> Dict[str, Any]: + """计算 MACD 指标""" + fast = params.get('fast_period', 12) + slow = params.get('slow_period', 26) + signal = params.get('signal_period', 9) + + close = hist['Close'] + + # 计算 EMA + ema_fast = close.ewm(span=fast, adjust=False).mean() + ema_slow = close.ewm(span=slow, adjust=False).mean() + + # MACD 线 = 快 EMA - 慢 EMA + macd_line = ema_fast - ema_slow + + # 信号线 = MACD 的 EMA + signal_line = macd_line.ewm(span=signal, adjust=False).mean() + + # 柱状图 = MACD - 信号线 + histogram = macd_line - signal_line + + # 获取最新值 + latest_macd = float(macd_line.iloc[-1]) + latest_signal = float(signal_line.iloc[-1]) + latest_hist = float(histogram.iloc[-1]) + prev_hist = float(histogram.iloc[-2]) if len(histogram) > 1 else 0 + + # 判断信号 + if latest_hist > 0 and prev_hist <= 0: + macd_signal = 'bullish_crossover' + interpretation = 'MACD 金叉 - 看涨信号' + elif latest_hist < 0 and prev_hist >= 0: + macd_signal = 'bearish_crossover' + interpretation = 'MACD 死叉 - 看跌信号' + elif latest_hist > 0: + macd_signal = 'bullish' + interpretation = 'MACD 位于信号线上方 - 多头趋势' + else: + macd_signal = 'bearish' + interpretation = 'MACD 位于信号线下方 - 空头趋势' + + return { + 'source': 'Yahoo Finance (计算)', + 'indicator': 'MACD', + 'macd_line': round(latest_macd, 4), + 'signal_line': round(latest_signal, 4), + 'histogram': round(latest_hist, 4), + 'signal': macd_signal, + 'interpretation': interpretation, + 'parameters': {'fast': fast, 'slow': slow, 'signal': signal}, + 'timestamp': hist.index[-1].isoformat() if hasattr(hist.index[-1], 'isoformat') else str(hist.index[-1]) + } + + def _calculate_sma(self, hist, params) -> Dict[str, Any]: + """计算简单移动平均线""" + period = params.get('time_period', 20) + close = hist['Close'] + sma = close.rolling(window=period).mean() + + return { + 'source': 'Yahoo Finance (计算)', + 'indicator': 'SMA', + 'value': round(float(sma.iloc[-1]), 4), + 'period': period, + 'timestamp': hist.index[-1].isoformat() if hasattr(hist.index[-1], 'isoformat') else str(hist.index[-1]) + } + + def _calculate_ema(self, hist, params) -> Dict[str, Any]: + """计算指数移动平均线""" + period = params.get('time_period', 20) + close = hist['Close'] + ema = close.ewm(span=period, adjust=False).mean() + + return { + 'source': 'Yahoo Finance (计算)', + 'indicator': 'EMA', + 'value': round(float(ema.iloc[-1]), 4), + 'period': period, + 'timestamp': hist.index[-1].isoformat() if hasattr(hist.index[-1], 'isoformat') else str(hist.index[-1]) + } + + +class AlphaVantageMCPSource(DataSource): + """ + Alpha Vantage MCP 数据源 + + 优势: + - 官方 API,数据稳定可靠 + - 提供预计算的 RSI、Bollinger Bands 等指标 + - 历史数据丰富(20+ 年) + + 限制: + - 免费版 15 分钟延时 + - 每天 25 次请求限制 + - MACD 需要 Premium 订阅 + """ + + def __init__(self): + self.name = "Alpha Vantage" + # 注意:实际调用通过 Claude MCP 工具完成 + # 这里只是数据格式化和处理逻辑 + + def get_quote(self, ticker: str) -> Dict[str, Any]: + """ + 获取 Alpha Vantage 报价 + + 注意:实际调用需要通过 MCP 工具: + mcp__Alpha-Vantage__TOOL_CALL( + tool_name="GLOBAL_QUOTE", + arguments={"symbol": ticker, "datatype": "json"} + ) + """ + # 这是数据格式化模板 + return { + 'source': self.name, + 'ticker': ticker.upper(), + 'price': None, # 从 MCP 响应填充 + 'open': None, + 'high': None, + 'low': None, + 'volume': None, + 'previous_close': None, + 'change': None, + 'change_percent': None, + 'timestamp': None, + 'realtime': False, # Alpha Vantage 免费版有 15 分钟延时 + 'mcp_tool': 'GLOBAL_QUOTE', + 'mcp_args': {'symbol': ticker, 'datatype': 'json'} + } + + def get_indicator(self, ticker: str, indicator: str, **params) -> Dict[str, Any]: + """ + 获取 Alpha Vantage 技术指标 + + 支持的免费指标: RSI, BBANDS, SMA, EMA, STOCH, ADX, CCI, AROON, MFI, OBV 等 + Premium 指标: MACD (需要订阅) + """ + indicator = indicator.upper() + + # 通用参数 + interval = params.get('interval', 'daily') + time_period = params.get('time_period', 14) + series_type = params.get('series_type', 'close') + + if indicator == 'RSI': + return { + 'source': self.name, + 'indicator': 'RSI', + 'mcp_tool': 'RSI', + 'mcp_args': { + 'symbol': ticker, + 'interval': interval, + 'time_period': time_period, + 'series_type': series_type, + 'datatype': 'json' + } + } + elif indicator in ['BBANDS', 'BOLLINGER']: + return { + 'source': self.name, + 'indicator': 'BBANDS', + 'mcp_tool': 'BBANDS', + 'mcp_args': { + 'symbol': ticker, + 'interval': interval, + 'time_period': params.get('time_period', 20), + 'series_type': series_type, + 'nbdevup': params.get('nbdevup', 2), + 'nbdevdn': params.get('nbdevdn', 2), + 'datatype': 'json' + } + } + elif indicator == 'SMA': + return { + 'source': self.name, + 'indicator': 'SMA', + 'mcp_tool': 'SMA', + 'mcp_args': { + 'symbol': ticker, + 'interval': interval, + 'time_period': time_period, + 'series_type': series_type, + 'datatype': 'json' + } + } + elif indicator == 'EMA': + return { + 'source': self.name, + 'indicator': 'EMA', + 'mcp_tool': 'EMA', + 'mcp_args': { + 'symbol': ticker, + 'interval': interval, + 'time_period': time_period, + 'series_type': series_type, + 'datatype': 'json' + } + } + else: + return { + 'source': self.name, + 'indicator': indicator, + 'error': f'指标 {indicator} 可能需要 Premium 订阅或不支持', + 'suggestion': '请使用 RSI, BBANDS, SMA, EMA 等免费指标' + } + + +class DataValidator: + """数据验证器 - 对比不同数据源的数据一致性""" + + PRICE_TOLERANCE = 0.01 # 价格容差 1% + + @staticmethod + def compare_quotes(yahoo_quote: Dict, av_quote: Dict) -> Dict[str, Any]: + """ + 对比 Yahoo Finance 和 Alpha Vantage 的报价数据 + + 返回对比结果,如果发现不一致会标记出来 + """ + result = { + 'comparison_time': datetime.now().isoformat(), + 'yahoo': yahoo_quote, + 'alpha_vantage': av_quote, + 'discrepancies': [], + 'aligned': True + } + + yahoo_price = yahoo_quote.get('price') + av_price = av_quote.get('price') + + if yahoo_price and av_price: + diff_pct = abs(yahoo_price - av_price) / yahoo_price * 100 + + if diff_pct > DataValidator.PRICE_TOLERANCE * 100: + result['discrepancies'].append({ + 'field': 'price', + 'yahoo_value': yahoo_price, + 'av_value': av_price, + 'difference_pct': round(diff_pct, 4), + 'warning': f'价格差异 {diff_pct:.2f}% 超过容差 {DataValidator.PRICE_TOLERANCE*100}%' + }) + result['aligned'] = False + + # 对比其他字段 + for field in ['open', 'high', 'low']: + y_val = yahoo_quote.get(field) + av_val = av_quote.get(field) + + if y_val and av_val: + diff_pct = abs(y_val - av_val) / y_val * 100 + if diff_pct > DataValidator.PRICE_TOLERANCE * 100: + result['discrepancies'].append({ + 'field': field, + 'yahoo_value': y_val, + 'av_value': av_val, + 'difference_pct': round(diff_pct, 4) + }) + result['aligned'] = False + + return result + + +class StockAnalyzer: + """ + 股票分析器 - 混合数据源架构 + + 数据源策略: + - 价格数据:优先 Yahoo Finance(实时) + - RSI, Bollinger Bands:Alpha Vantage MCP(预计算,准确) + - MACD:Yahoo Finance + pandas 计算(AV 免费版不支持) + - 数据验证:对比两个数据源确保一致性 + """ + + def __init__(self, config: Optional[Dict] = None): + self.config = config or self._default_config() + self.yahoo = YahooFinanceSource() + self.alpha_vantage = AlphaVantageMCPSource() + self.validator = DataValidator() + + print(f"[股票分析器] 已初始化 - 混合数据源模式") + print(f" - 价格数据: Yahoo Finance (实时)") + print(f" - RSI/Bollinger: Alpha Vantage MCP") + print(f" - MACD: Yahoo Finance (本地计算)") + + def analyze( + self, + ticker: str, + indicators: Optional[List[str]] = None, + period: str = "3mo", + validate_data: bool = True + ) -> Dict[str, Any]: + """ + 执行股票技术分析 + + 参数: + ticker: 股票代码 (如 "AAPL", "MSFT") + indicators: 要计算的指标列表 (默认: ["RSI", "MACD", "Bollinger"]) + period: 分析时间周期 (默认: "3mo") + validate_data: 是否对比验证数据源 (默认: True) + + 返回: + 包含价格、指标、信号和建议的分析结果字典 + """ + indicators = indicators or ["RSI", "MACD", "Bollinger"] + + print(f"\n[股票分析器] 正在分析 {ticker}...") + print(f" - 指标: {indicators}") + print(f" - 周期: {period}") + + result = { + 'ticker': ticker.upper(), + 'timestamp': datetime.now().isoformat(), + 'data_sources': { + 'price': 'Yahoo Finance', + 'indicators': {} + }, + 'price_data': {}, + 'indicators': {}, + 'signal': {}, + 'validation': None + } + + # 步骤 1: 获取价格数据 (Yahoo - 实时) + try: + result['price_data'] = self.yahoo.get_quote(ticker) + print(f" ✓ 价格数据获取成功: ${result['price_data'].get('price', 'N/A')}") + except Exception as e: + result['price_data'] = {'error': str(e)} + print(f" ✗ 价格数据获取失败: {e}") + + # 步骤 2: 获取技术指标 + for indicator in indicators: + indicator_upper = indicator.upper() + + if indicator_upper == 'MACD': + # MACD 使用 Yahoo + 本地计算 + try: + result['indicators']['MACD'] = self.yahoo.get_indicator( + ticker, 'MACD', + period=period, + fast_period=self.config['indicators']['MACD']['fast_period'], + slow_period=self.config['indicators']['MACD']['slow_period'], + signal_period=self.config['indicators']['MACD']['signal_period'] + ) + result['data_sources']['indicators']['MACD'] = 'Yahoo Finance (本地计算)' + print(f" ✓ MACD 计算成功") + except Exception as e: + result['indicators']['MACD'] = {'error': str(e)} + print(f" ✗ MACD 计算失败: {e}") + + elif indicator_upper in ['RSI', 'BOLLINGER', 'BBANDS']: + # RSI 和 Bollinger 使用 Alpha Vantage MCP + indicator_name = 'BBANDS' if indicator_upper in ['BOLLINGER', 'BBANDS'] else indicator_upper + + if indicator_name == 'RSI': + mcp_config = self.alpha_vantage.get_indicator( + ticker, 'RSI', + time_period=self.config['indicators']['RSI']['period'] + ) + else: + mcp_config = self.alpha_vantage.get_indicator( + ticker, 'BBANDS', + time_period=self.config['indicators']['Bollinger']['period'], + nbdevup=self.config['indicators']['Bollinger']['std_dev'], + nbdevdn=self.config['indicators']['Bollinger']['std_dev'] + ) + + result['indicators'][indicator_name] = mcp_config + result['data_sources']['indicators'][indicator_name] = 'Alpha Vantage MCP' + print(f" → {indicator_name} 需要调用 MCP 工具: {mcp_config.get('mcp_tool')}") + + else: + # 其他指标尝试 Alpha Vantage + mcp_config = self.alpha_vantage.get_indicator(ticker, indicator) + result['indicators'][indicator] = mcp_config + result['data_sources']['indicators'][indicator] = 'Alpha Vantage MCP' + + # 步骤 3: 生成交易信号 + result['signal'] = self._generate_signal(ticker, result['price_data'], result['indicators']) + + # 步骤 4: 数据验证(可选) + if validate_data: + result['validation'] = { + 'enabled': True, + 'note': '数据验证需要同时调用 Alpha Vantage GLOBAL_QUOTE 进行对比', + 'av_quote_config': self.alpha_vantage.get_quote(ticker) + } + + print(f"\n[股票分析器] 分析完成") + print(f" → 信号: {result['signal'].get('action', 'N/A')} ({result['signal'].get('confidence', 'N/A')})") + + return result + + def compare( + self, + tickers: List[str], + rank_by: str = "momentum", + indicators: Optional[List[str]] = None + ) -> Dict[str, Any]: + """ + 对比多只股票并按技术强度排名 + + 参数: + tickers: 股票代码列表 + rank_by: 排名方式 ("momentum", "rsi", "composite") + indicators: 用于对比的指标 + + 返回: + 包含排名结果的字典 + """ + indicators = indicators or ["RSI", "MACD"] + + print(f"\n[股票分析器] 对比 {len(tickers)} 只股票...") + print(f" - 股票: {', '.join(tickers)}") + print(f" - 排名方式: {rank_by}") + + comparisons = [] + for ticker in tickers: + analysis = self.analyze(ticker, indicators, validate_data=False) + score = self._calculate_ranking_score(analysis, rank_by) + + comparisons.append({ + 'ticker': ticker.upper(), + 'analysis': analysis, + 'score': score, + 'rank': 0 + }) + + # 按分数排序 + comparisons.sort(key=lambda x: x['score'], reverse=True) + + # 分配排名 + for idx, comp in enumerate(comparisons, 1): + comp['rank'] = idx + + result = { + 'ranked_stocks': comparisons, + 'ranking_method': rank_by, + 'total_analyzed': len(tickers), + 'timestamp': datetime.now().isoformat() + } + + print(f"\n[股票分析器] 对比完成") + print(" 排名结果:") + for comp in comparisons: + print(f" #{comp['rank']}: {comp['ticker']} (分数: {comp['score']:.2f})") + + return result + + def validate_data_sources(self, ticker: str) -> Dict[str, Any]: + """ + 验证两个数据源的数据一致性 + + 这个方法会同时从 Yahoo Finance 和 Alpha Vantage 获取数据, + 并对比关键字段。如果发现不一致会返回警告。 + + 返回: + 包含对比结果和任何差异警告的字典 + """ + print(f"\n[数据验证] 正在验证 {ticker} 的数据源一致性...") + + # 获取 Yahoo 数据 + yahoo_quote = self.yahoo.get_quote(ticker) + + # Alpha Vantage 需要通过 MCP 调用 + av_config = self.alpha_vantage.get_quote(ticker) + + return { + 'ticker': ticker.upper(), + 'timestamp': datetime.now().isoformat(), + 'yahoo_quote': yahoo_quote, + 'alpha_vantage_config': av_config, + 'instruction': '请使用 MCP 工具获取 Alpha Vantage 数据后调用 compare_quotes() 进行对比', + 'note': '如果价格差异超过 1%,将会触发警告' + } + + def _default_config(self) -> Dict: + """默认配置""" + return { + 'data_sources': { + 'price': 'yahoo_finance', + 'rsi': 'alpha_vantage', + 'macd': 'yahoo_finance', + 'bollinger': 'alpha_vantage' + }, + 'indicators': { + 'RSI': { + 'period': 14, + 'overbought': 70, + 'oversold': 30 + }, + 'MACD': { + 'fast_period': 12, + 'slow_period': 26, + 'signal_period': 9 + }, + 'Bollinger': { + 'period': 20, + 'std_dev': 2 + } + }, + 'validation': { + 'enabled': True, + 'price_tolerance': 0.01 # 1% + } + } + + def _generate_signal( + self, + ticker: str, + price_data: Dict, + indicators: Dict + ) -> Dict[str, Any]: + """ + 根据技术指标生成交易信号 + + 策略: RSI + MACD 组合判断 + - 买入: RSI 超卖 + MACD 金叉 + - 卖出: RSI 超买 + MACD 死叉 + - 持有: 其他情况 + """ + reasoning = [] + base_signal = "HOLD" + confidence = "low" + + # RSI 分析 + rsi_data = indicators.get('RSI', {}) + if 'mcp_args' not in rsi_data: # 如果已经有实际数据 + rsi_value = rsi_data.get('value') + if rsi_value: + if rsi_value < 30: + reasoning.append(f"RSI 超卖 ({rsi_value:.1f} < 30) - 潜在买入机会") + base_signal = "BUY" + confidence = "moderate" + elif rsi_value > 70: + reasoning.append(f"RSI 超买 ({rsi_value:.1f} > 70) - 潜在卖出信号") + base_signal = "SELL" + confidence = "moderate" + else: + reasoning.append(f"RSI 中性区间 ({rsi_value:.1f})") + else: + reasoning.append("RSI 需要通过 MCP 获取") + + # MACD 分析 + macd_data = indicators.get('MACD', {}) + macd_signal = macd_data.get('signal') + if macd_signal: + if macd_signal == 'bullish_crossover': + reasoning.append("MACD 金叉 - 看涨信号") + if base_signal == "BUY": + confidence = "high" + elif base_signal == "HOLD": + base_signal = "BUY" + confidence = "moderate" + elif macd_signal == 'bearish_crossover': + reasoning.append("MACD 死叉 - 看跌信号") + if base_signal == "SELL": + confidence = "high" + elif base_signal == "HOLD": + base_signal = "SELL" + confidence = "moderate" + elif macd_signal == 'bullish': + reasoning.append("MACD 多头趋势") + else: + reasoning.append("MACD 空头趋势") + + # Bollinger Bands 分析 + bb_data = indicators.get('BBANDS', {}) + if 'mcp_args' not in bb_data: + # 如果有实际数据可以分析 + pass + + return { + 'action': base_signal, + 'confidence': confidence, + 'reasoning': reasoning, + 'price': price_data.get('price'), + 'timestamp': datetime.now().isoformat() + } + + def _calculate_ranking_score(self, analysis: Dict, method: str) -> float: + """计算排名分数""" + indicators = analysis.get('indicators', {}) + + if method == "rsi": + rsi_data = indicators.get('RSI', {}) + rsi = rsi_data.get('value', 50) + return min(rsi, 70) if rsi else 50 + + elif method == "momentum": + score = 50 + + # RSI 贡献 + rsi_data = indicators.get('RSI', {}) + if 'value' in rsi_data: + score = rsi_data['value'] + + # MACD 贡献 + macd_data = indicators.get('MACD', {}) + macd_signal = macd_data.get('signal') + if macd_signal == 'bullish_crossover': + score += 15 + elif macd_signal == 'bullish': + score += 5 + elif macd_signal == 'bearish_crossover': + score -= 15 + elif macd_signal == 'bearish': + score -= 5 + + return score + + else: # composite + rsi_data = indicators.get('RSI', {}) + rsi = rsi_data.get('value', 50) + macd_data = indicators.get('MACD', {}) + macd_hist = macd_data.get('histogram', 0) + + return (rsi * 0.6) + (macd_hist * 20 * 0.4) + + +def format_av_response(av_response: Dict, indicator: str) -> Dict[str, Any]: + """ + 格式化 Alpha Vantage MCP 响应 + + 用于处理从 MCP 工具返回的原始数据 + """ + if 'error' in av_response or 'Information' in av_response: + return {'error': av_response.get('error') or av_response.get('Information')} + + if indicator == 'RSI': + meta = av_response.get('Meta Data', {}) + data = av_response.get('Technical Analysis: RSI', {}) + + if data: + latest_date = list(data.keys())[0] + latest_value = float(data[latest_date]['RSI']) + + # 判断信号 + if latest_value < 30: + signal = 'oversold' + interpretation = f'RSI 超卖 ({latest_value:.1f}) - 潜在买入机会' + elif latest_value > 70: + signal = 'overbought' + interpretation = f'RSI 超买 ({latest_value:.1f}) - 潜在卖出信号' + else: + signal = 'neutral' + interpretation = f'RSI 中性 ({latest_value:.1f})' + + return { + 'source': 'Alpha Vantage', + 'indicator': 'RSI', + 'value': latest_value, + 'signal': signal, + 'interpretation': interpretation, + 'timestamp': latest_date + } + + elif indicator == 'BBANDS': + meta = av_response.get('Meta Data', {}) + data = av_response.get('Technical Analysis: BBANDS', {}) + + if data: + latest_date = list(data.keys())[0] + latest = data[latest_date] + + return { + 'source': 'Alpha Vantage', + 'indicator': 'BBANDS', + 'upper_band': float(latest['Real Upper Band']), + 'middle_band': float(latest['Real Middle Band']), + 'lower_band': float(latest['Real Lower Band']), + 'timestamp': latest_date + } + + elif indicator == 'GLOBAL_QUOTE': + quote = av_response.get('Global Quote', {}) + if quote: + return { + 'source': 'Alpha Vantage', + 'ticker': quote.get('01. symbol'), + 'price': float(quote.get('05. price', 0)), + 'open': float(quote.get('02. open', 0)), + 'high': float(quote.get('03. high', 0)), + 'low': float(quote.get('04. low', 0)), + 'volume': int(quote.get('06. volume', 0)), + 'previous_close': float(quote.get('08. previous close', 0)), + 'change': float(quote.get('09. change', 0)), + 'change_percent': quote.get('10. change percent'), + 'timestamp': quote.get('07. latest trading day'), + 'realtime': False + } + + return av_response + + +def main(): + """演示混合数据源股票分析""" + print("=" * 60) + print("股票分析器 - 混合数据源演示") + print("=" * 60) + + analyzer = StockAnalyzer() + + # 示例 1: 单股分析 + print("\n--- 示例 1: 分析 AAPL ---") + result = analyzer.analyze("AAPL", ["RSI", "MACD", "Bollinger"]) + + print("\n分析结果摘要:") + print(f" 股票: {result['ticker']}") + print(f" 价格: ${result['price_data'].get('price', 'N/A')}") + print(f" 信号: {result['signal']['action']}") + print(f" 置信度: {result['signal']['confidence']}") + print(f" 理由: {', '.join(result['signal']['reasoning'])}") + + print("\n" + "=" * 60) + print("演示完成!") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/skills/stock-master/scripts/portfolio.py b/skills/stock-master/scripts/portfolio.py new file mode 100644 index 00000000..c5368b31 --- /dev/null +++ b/skills/stock-master/scripts/portfolio.py @@ -0,0 +1,424 @@ +""" +股票持仓管理模块 + +功能: +- 创建 Excel 持仓模板 +- 读取持仓数据 +- 计算实时盈亏 +- 记录交易历史 + +配置说明: +通过环境变量 PORTFOLIO_PATH 或 config.json 配置持仓文件路径 +""" + +from typing import Dict, List, Optional, Any +from datetime import datetime +from pathlib import Path +import json +import os + + +def get_portfolio_path(output_path: str = None) -> Path: + """获取持仓文件路径""" + if output_path: + return Path(output_path) + + # 优先使用环境变量 + if os.environ.get('PORTFOLIO_PATH'): + return Path(os.environ['PORTFOLIO_PATH']) + + # 其次使用配置文件 + config_path = Path('./config.json') + if config_path.exists(): + with open(config_path, 'r', encoding='utf-8') as f: + config = json.load(f) + if config.get('portfolio_path'): + return Path(config['portfolio_path']) + + # 默认使用当前目录 + return Path('./my_portfolio.xlsx') + + +def create_portfolio_template(output_path: str = None) -> str: + """ + 创建 Excel 持仓模板 + + 参数: + output_path: 输出路径,默认为当前目录的 my_portfolio.xlsx + + 返回: + 创建的文件路径 + """ + try: + from openpyxl import Workbook + from openpyxl.styles import Font, Alignment, PatternFill, Border, Side + from openpyxl.utils import get_column_letter + except ImportError: + raise ImportError("请安装 openpyxl: pip install openpyxl") + + output_path = get_portfolio_path(output_path) + output_path.parent.mkdir(parents=True, exist_ok=True) + + wb = Workbook() + + # ===== Sheet 1: 持仓 ===== + ws_holdings = wb.active + ws_holdings.title = "持仓" + + # 表头样式 + header_font = Font(bold=True, color="FFFFFF") + header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid") + header_alignment = Alignment(horizontal="center", vertical="center") + thin_border = Border( + left=Side(style='thin'), + right=Side(style='thin'), + top=Side(style='thin'), + bottom=Side(style='thin') + ) + + # 持仓表头 + holdings_headers = [ + "股票代码", "股票名称", "持仓数量", "买入均价", + "当前价格", "市值", "盈亏金额", "盈亏比例", "买入日期", "备注" + ] + + for col, header in enumerate(holdings_headers, 1): + cell = ws_holdings.cell(row=1, column=col, value=header) + cell.font = header_font + cell.fill = header_fill + cell.alignment = header_alignment + cell.border = thin_border + + # 示例数据 + sample_holdings = [ + ["AAPL", "苹果公司", 10, 180.00, "", "", "", "", "2024-01-15", "长期持有"], + ["TSLA", "特斯拉", 5, 400.00, "", "", "", "", "2024-06-01", ""], + ["GOOGL", "谷歌", 3, 150.00, "", "", "", "", "2024-03-20", ""], + ] + + for row_idx, row_data in enumerate(sample_holdings, 2): + for col_idx, value in enumerate(row_data, 1): + cell = ws_holdings.cell(row=row_idx, column=col_idx, value=value) + cell.border = thin_border + if col_idx >= 5: # 当前价格及之后的列 + cell.alignment = Alignment(horizontal="right") + + # 添加公式说明 + ws_holdings.cell(row=6, column=1, value="说明:").font = Font(bold=True, color="FF0000") + ws_holdings.cell(row=7, column=1, value="• 股票代码:美股用代码如 AAPL,港股用 0700.HK,A股用 600519.SS") + ws_holdings.cell(row=8, column=1, value="• 当前价格、市值、盈亏:由 Claude 自动更新填写") + ws_holdings.cell(row=9, column=1, value="• 只需填写:股票代码、持仓数量、买入均价、买入日期") + + # 调整列宽 + column_widths = [12, 15, 12, 12, 12, 15, 15, 12, 12, 20] + for idx, width in enumerate(column_widths, 1): + ws_holdings.column_dimensions[get_column_letter(idx)].width = width + + # ===== Sheet 2: 交易记录 ===== + ws_transactions = wb.create_sheet("交易记录") + + transaction_headers = [ + "日期", "股票代码", "操作", "数量", "价格", "金额", "手续费", "备注" + ] + + for col, header in enumerate(transaction_headers, 1): + cell = ws_transactions.cell(row=1, column=col, value=header) + cell.font = header_font + cell.fill = header_fill + cell.alignment = header_alignment + cell.border = thin_border + + # 示例交易 + sample_transactions = [ + ["2024-01-15", "AAPL", "买入", 10, 180.00, 1800.00, 0, "首次建仓"], + ["2024-03-20", "GOOGL", "买入", 3, 150.00, 450.00, 0, ""], + ["2024-06-01", "TSLA", "买入", 5, 400.00, 2000.00, 0, ""], + ] + + for row_idx, row_data in enumerate(sample_transactions, 2): + for col_idx, value in enumerate(row_data, 1): + cell = ws_transactions.cell(row=row_idx, column=col_idx, value=value) + cell.border = thin_border + + # 调整列宽 + trans_widths = [12, 12, 8, 10, 12, 12, 10, 25] + for idx, width in enumerate(trans_widths, 1): + ws_transactions.column_dimensions[get_column_letter(idx)].width = width + + # ===== Sheet 3: 账户汇总 ===== + ws_summary = wb.create_sheet("账户汇总") + + summary_data = [ + ["账户汇总", ""], + ["", ""], + ["总投入", "=SUM(交易记录!F:F)"], + ["当前市值", "(由 Claude 更新)"], + ["总盈亏", "(由 Claude 更新)"], + ["收益率", "(由 Claude 更新)"], + ["", ""], + ["最后更新时间", datetime.now().strftime("%Y-%m-%d %H:%M")], + ] + + ws_summary.cell(row=1, column=1, value="账户汇总").font = Font(bold=True, size=14) + + for row_idx, (label, value) in enumerate(summary_data[2:], 3): + ws_summary.cell(row=row_idx, column=1, value=label).font = Font(bold=True) + ws_summary.cell(row=row_idx, column=2, value=value) + + ws_summary.column_dimensions['A'].width = 15 + ws_summary.column_dimensions['B'].width = 20 + + # ===== Sheet 4: 使用说明 ===== + ws_help = wb.create_sheet("使用说明") + + help_text = [ + ["股票持仓管理表 - 使用说明"], + [""], + ["一、持仓表(必填项)"], + [" • 股票代码:填写股票代码"], + [" - 美股:直接填代码,如 AAPL, TSLA, GOOGL"], + [" - 港股:代码后加 .HK,如 0700.HK, 9988.HK"], + [" - A股:代码后加 .SS(上海) 或 .SZ(深圳),如 600519.SS"], + [" • 持仓数量:当前实际持有的股数"], + [" • 买入均价:当前持仓的平均成本价(做T后的实际均价)"], + [" • 买入日期:首次建仓日期 或 最近一次加仓日期"], + [""], + ["二、做T操作说明"], + [" • 持仓表只记录「结果」,不需要记录每次操作"], + [" • 例如:TSLA 做了几轮T,最终持仓100股,均价$380"], + [" → 直接填:数量=100,均价=380"], + [" • 均价可从券商App查看,或自己计算"], + [" • 想追踪每次操作盈亏,可在「交易记录」表记录(可选)"], + [""], + ["三、自动更新项"], + [" • 当前价格:Claude 会自动获取实时价格填入"], + [" • 市值 = 持仓数量 × 当前价格"], + [" • 盈亏金额 = 市值 - (持仓数量 × 买入均价)"], + [" • 盈亏比例 = 盈亏金额 / (持仓数量 × 买入均价) × 100%"], + [""], + ["四、交易记录(可选)"], + [" • 每次买入/卖出后,在交易记录表添加一行"], + [" • 操作类型:买入 / 卖出"], + [" • 不记录也不影响持仓分析"], + [""], + ["五、使用 Claude 分析"], + [" • 告诉 Claude:'分析我的持仓'"], + [" • Claude 会读取此表格,更新价格并给出建议"], + ] + + for row_idx, row in enumerate(help_text, 1): + ws_help.cell(row=row_idx, column=1, value=row[0] if row else "") + + ws_help.cell(row=1, column=1).font = Font(bold=True, size=14) + ws_help.column_dimensions['A'].width = 60 + + # 保存文件 + wb.save(output_path) + print(f"✅ 持仓模板已创建: {output_path}") + + return str(output_path) + + +def read_portfolio(file_path: str = None) -> Dict[str, Any]: + """ + 读取 Excel 持仓文件 + + 参数: + file_path: Excel 文件路径 + + 返回: + 包含持仓数据的字典 + """ + try: + from openpyxl import load_workbook + except ImportError: + raise ImportError("请安装 openpyxl: pip install openpyxl") + + file_path = get_portfolio_path(file_path) + wb = load_workbook(file_path, data_only=True) + + # 读取持仓表 + ws_holdings = wb["持仓"] + + holdings = [] + for row in ws_holdings.iter_rows(min_row=2, max_row=ws_holdings.max_row): + # 跳过空行或说明行 + ticker = row[0].value + if not ticker or ticker.startswith("说明") or ticker.startswith("•"): + continue + + holding = { + 'ticker': str(ticker).upper(), + 'name': row[1].value or "", + 'shares': float(row[2].value or 0), + 'avg_cost': float(row[3].value or 0), + 'current_price': row[4].value, # 可能为空 + 'market_value': row[5].value, + 'profit_loss': row[6].value, + 'profit_loss_pct': row[7].value, + 'buy_date': str(row[8].value) if row[8].value else "", + 'notes': row[9].value or "" + } + + if holding['shares'] > 0: + holdings.append(holding) + + # 读取交易记录 + ws_trans = wb["交易记录"] + + transactions = [] + for row in ws_trans.iter_rows(min_row=2, max_row=ws_trans.max_row): + date = row[0].value + if not date: + continue + + transaction = { + 'date': str(date), + 'ticker': str(row[1].value or "").upper(), + 'action': row[2].value or "", + 'shares': float(row[3].value or 0), + 'price': float(row[4].value or 0), + 'amount': float(row[5].value or 0), + 'fee': float(row[6].value or 0), + 'notes': row[7].value or "" + } + transactions.append(transaction) + + wb.close() + + return { + 'holdings': holdings, + 'transactions': transactions, + 'file_path': str(file_path), + 'read_time': datetime.now().isoformat() + } + + +def update_portfolio_prices(file_path: str, prices: Dict[str, float]) -> Dict[str, Any]: + """ + 更新持仓表中的当前价格和盈亏 + + 参数: + file_path: Excel 文件路径 + prices: 股票代码 -> 当前价格 的字典 + + 返回: + 更新后的持仓汇总 + """ + try: + from openpyxl import load_workbook + except ImportError: + raise ImportError("请安装 openpyxl: pip install openpyxl") + + file_path = get_portfolio_path(file_path) + wb = load_workbook(file_path) + ws_holdings = wb["持仓"] + + total_cost = 0 + total_value = 0 + updated_holdings = [] + + for row_idx in range(2, ws_holdings.max_row + 1): + ticker = ws_holdings.cell(row=row_idx, column=1).value + if not ticker or ticker.startswith("说明") or ticker.startswith("•"): + continue + + ticker = str(ticker).upper() + shares = float(ws_holdings.cell(row=row_idx, column=3).value or 0) + avg_cost = float(ws_holdings.cell(row=row_idx, column=4).value or 0) + + if shares <= 0: + continue + + # 获取当前价格 + current_price = prices.get(ticker) + if current_price: + # 计算市值和盈亏 + cost_basis = shares * avg_cost + market_value = shares * current_price + profit_loss = market_value - cost_basis + profit_loss_pct = (profit_loss / cost_basis * 100) if cost_basis > 0 else 0 + + # 更新单元格 + ws_holdings.cell(row=row_idx, column=5, value=round(current_price, 2)) + ws_holdings.cell(row=row_idx, column=6, value=round(market_value, 2)) + ws_holdings.cell(row=row_idx, column=7, value=round(profit_loss, 2)) + ws_holdings.cell(row=row_idx, column=8, value=f"{profit_loss_pct:.2f}%") + + total_cost += cost_basis + total_value += market_value + + updated_holdings.append({ + 'ticker': ticker, + 'shares': shares, + 'avg_cost': avg_cost, + 'current_price': current_price, + 'market_value': market_value, + 'profit_loss': profit_loss, + 'profit_loss_pct': profit_loss_pct + }) + + # 更新汇总表 + ws_summary = wb["账户汇总"] + ws_summary.cell(row=4, column=2, value=round(total_value, 2)) + ws_summary.cell(row=5, column=2, value=round(total_value - total_cost, 2)) + total_return = ((total_value - total_cost) / total_cost * 100) if total_cost > 0 else 0 + ws_summary.cell(row=6, column=2, value=f"{total_return:.2f}%") + ws_summary.cell(row=8, column=2, value=datetime.now().strftime("%Y-%m-%d %H:%M")) + + wb.save(file_path) + wb.close() + + return { + 'total_cost': total_cost, + 'total_value': total_value, + 'total_profit_loss': total_value - total_cost, + 'total_return_pct': total_return, + 'holdings': updated_holdings, + 'update_time': datetime.now().isoformat() + } + + +def format_portfolio_summary(portfolio_data: Dict[str, Any]) -> str: + """ + 格式化持仓汇总为易读文本 + """ + holdings = portfolio_data.get('holdings', []) + + if not holdings: + return "持仓为空,请先添加股票" + + lines = [ + "**持仓汇总**", + "", + "| 股票 | 数量 | 成本价 | 现价 | 盈亏 |", + "|------|------|--------|------|------|" + ] + + for h in holdings: + profit_emoji = "+" if h.get('profit_loss', 0) >= 0 else "" + lines.append( + f"| {h['ticker']} | {h['shares']:.0f} | ${h['avg_cost']:.2f} | " + f"${h.get('current_price', 0):.2f} | {profit_emoji}{h.get('profit_loss_pct', 0):.1f}% |" + ) + + total = portfolio_data.get('total_profit_loss', 0) + total_pct = portfolio_data.get('total_return_pct', 0) + total_sign = "+" if total >= 0 else "" + + lines.extend([ + "", + f"**总投入**: ${portfolio_data.get('total_cost', 0):,.2f}", + f"**当前市值**: ${portfolio_data.get('total_value', 0):,.2f}", + f"**总盈亏**: {total_sign}${total:,.2f} ({total_sign}{total_pct:.2f}%)" + ]) + + return "\n".join(lines) + + +if __name__ == "__main__": + # 测试创建模板 + path = create_portfolio_template() + print(f"\n测试读取: {path}") + data = read_portfolio(path) + print(f"读取到 {len(data['holdings'])} 条持仓记录")