diff --git a/README.md b/README.md index f8599d5..b23e446 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,36 @@ easy-tdx server-info --table easy-tdx symbol-info SZ 000001 --table ``` +### 技术指标 + +```bash +easy-tdx indicator-list --table # 列出所有可用指标 +easy-tdx indicator MACD -m SH -c 600519 --table # MACD +easy-tdx indicator KDJ -m SZ -c 000001 --table # KDJ +easy-tdx indicator RSI -m SH -c 600519 --table # RSI +easy-tdx indicator BOLL -m SH -c 600519 --table # BOLL 布林带 +easy-tdx indicator DMI -m SH -c 600519 --table # DMI 动向指标 +easy-tdx indicator ATR -m SH -c 600519 --table # ATR 真实波幅 +easy-tdx indicator WR -m SH -c 600519 --table # WR 威廉指标 +easy-tdx indicator CCI -m SH -c 600519 --table # CCI 顺势指标 +easy-tdx indicator BIAS -m SZ -c 000001 --table # BIAS 乖离率 +easy-tdx indicator OBV -m SZ -c 000001 --table # OBV 能量潮 + +# 多指标同时计算 +easy-tdx indicator MACD,KDJ,RSI,BOLL -m SH -c 600519 --count 10 --table + +# 自定义参数 +easy-tdx indicator MACD -m SH -c 600519 --params SHORT=10,LONG=22 + +# 分钟线指标 +easy-tdx indicator MACD -m SH -c 600519 --period 5MIN --count 50 + +# 仅输出指标值(不含 OHLCV) +easy-tdx indicator RSI -m SZ -c 000001 --no-ohlcv +``` + +支持 30 个指标:MACD, KDJ, RSI, BOLL, DMI, ATR, WR, CCI, BIAS, OBV, VR, EMV, MFI, BRAR, ASI, TRIX, DPO, MTM, ROC, EXPMA, BBI, PSY, DFMA, CR, KTN, XSII, MASS, TAQ。 + ### 财务 ```bash @@ -125,6 +155,8 @@ easy-tdx ex tick HK_MAIN_BOARD 00700 --table # 港股分时 | `market-stat` | 全市场涨跌统计 | | `server-info` | 服务器交易时段 | | `symbol-info` | 个股特征快照 | +| `indicator` | 技术指标计算(30 个:MACD/KDJ/RSI/BOLL/DMI/ATR...) | +| `indicator-list` | 列出可用技术指标 | | `f10` | F10 公司信息 | | `fund-flow` | 历史资金流向 | | `ex kline` | 扩展市场 K 线 | @@ -188,6 +220,74 @@ with MacClient.from_best_host() as c: 返回列:`datetime, open, close, high, low, vol, amount`。 +#### 技术指标 + +自动获取 200+ 条历史数据预热 EMA,返回最后 `count` 条带指标的结果: + +```python +from easy_tdx import MacClient, Market, Period, Adjust +from easy_tdx.indicator import compute_indicators, list_indicators + +with MacClient.from_best_host() as c: + # 便捷方法:获取 K 线 + 计算指标一步完成(默认前复权) + df = c.get_stock_kline_with_indicators( + Market.SH, "600519", + indicators=["MACD", "KDJ", "RSI", "BOLL"], + count=30, + ) + # df 包含: datetime, open, close, high, low, vol, amount + # + MACD_DIF, MACD_DEA, MACD_HIST, KDJ_K, KDJ_D, KDJ_J, RSI, + # BOLL_UPPER, BOLL_MID, BOLL_LOWER + + # 自定义指标参数 + df = c.get_stock_kline_with_indicators( + Market.SH, "600519", + indicators=["MACD"], + params={"MACD": {"SHORT": 10, "LONG": 22}}, + ) + + # 独立使用:对已有 DataFrame 计算指标 + raw = c.get_stock_kline(Market.SH, "600519", Period.DAILY, count=200, adjust=Adjust.QFQ) + result = compute_indicators(raw, ["ATR", "CCI", "WR"], tail=30) + + # 查看所有可用指标 + for info in list_indicators(): + print(info["name"], info["description"], info["outputs"]) +``` + +支持 30 个技术指标: + +| 指标 | 输入 | 输出列 | +|------|------|--------| +| MACD | close | MACD_DIF, MACD_DEA, MACD_HIST | +| KDJ | close, high, low | KDJ_K, KDJ_D, KDJ_J | +| RSI | close | RSI | +| BOLL | close | BOLL_UPPER, BOLL_MID, BOLL_LOWER | +| DMI | close, high, low | DMI_PDI, DMI_MDI, DMI_ADX, DMI_ADXR | +| ATR | close, high, low | ATR | +| WR | close, high, low | WR1, WR2 | +| CCI | close, high, low | CCI | +| BIAS | close | BIAS1, BIAS2, BIAS3 | +| OBV | close, vol | OBV | +| VR | close, vol | VR | +| EMV | high, low, vol | EMV, EMV_MA | +| MFI | close, high, low, vol | MFI | +| BRAR | open, close, high, low | AR, BR | +| ASI | open, close, high, low | ASI, ASI_MA | +| TRIX | close | TRIX, TRIX_MA | +| DPO | close | DPO, DPO_MA | +| MTM | close | MTM, MTM_MA | +| ROC | close | ROC, ROC_MA | +| EXPMA | close | EXPMA_12, EXPMA_50 | +| BBI | close | BBI | +| PSY | close | PSY, PSY_MA | +| DFMA | close | DFMA_DIF, DFMA_DMA | +| CR | close, high, low | CR | +| KTN | close, high, low | KTN_UPPER, KTN_MID, KTN_LOWER | +| XSII | close, high, low | XSII_TD1, XSII_TD2, XSII_TD3, XSII_TD4 | +| MASS | high, low | MASS, MASS_MA | +| TAQ | high, low | TAQ_UP, TAQ_MID, TAQ_DOWN | + #### 分时 ```python @@ -303,6 +403,26 @@ with TdxClient.from_best_host() as c: `AsyncTdxClient` 提供对应的 `async def` 方法,接口一一对应。 +### SecurityQuote 字段说明 + +`get_security_quotes()` 返回的 DataFrame 包含以下特殊字段: + +| 字段 | 类型 | 说明 | +|------|------|------| +| `trading_status` | int | 交易状态标志。`0x8020`(32800) = 停牌,其余值表示正常交易或集合竞价 | +| `open_amount` | float | 集合竞价成交金额(元)。仅个股有效,指数该字段无意义 | +| `server_time` | str | 服务器时间,格式 `HH:MM:SS.mmm` | +| `unknown_2` | int | 指数: 集合竞价成交金额/100;个股: 舍入残差≈0 | +| `unknown_3` | int | 个股: 集合竞价成交金额/100;指数: 负值/无意义 | +| `unknown_5-8` | int | 保留字段,恒为 0 | + +检测停牌: + +```python +df = c.get_security_quotes([(Market.SH, "600000")]) +is_suspended = df.iloc[0]["trading_status"] == 0x8020 +``` + ### 离线数据读取 无需网络,从本地通达信安装目录直接读取: @@ -407,6 +527,7 @@ bars = read_daily_bars(filepath) | `get_stock_quotes(stocks, fields)` | 批量实时报价 | | `get_stock_quotes_list(category, ...)` | 市场分类排序报价 | | `get_stock_kline(market, code, period, ...)` | K 线(支持复权) | +| `get_stock_kline_with_indicators(market, code, indicators, ...)` | K 线 + 技术指标 | | `get_tick_chart(market, code, date)` | 单日分时图 | | `get_tick_charts(market, code, days)` | 多日分时图 | | `get_chart_sampling(market, code)` | 分时缩略采样 | @@ -469,6 +590,8 @@ src/easy_tdx/ ├── client.py # TdxClient / AsyncTdxClient(标准协议) ├── unified.py # UnifiedTdxClient(统一入口) ├── config.py # 服务器地址、端口、超时配置 +├── indicator.py # 技术指标计算(30 个,基于 MyTT) +├── MyTT.py # 麦语言技术指标算法库 ├── mac/ │ ├── client.py # MacClient / AsyncMacClient(MAC 协议) │ ├── enums.py # Period, Adjust, Category, ExMarket, SortType, ... @@ -505,5 +628,44 @@ ruff format --check src/ tests/ # format check - [pytdx](https://github.com/rainx/pytdx) -- 离线数据读取模块借鉴自 pytdx 项目,感谢 rainx 及所有贡献者 - [xmtdx](https://github.com/minionszyw/xmtdx) -- 本项目初始原型 - [mootdx](https://github.com/mootdx/mootdx) -- 工程化封装参考 +- [MyTT](https://github.com/mpquant/MyTT) -- 麦语言技术指标算法库,技术指标计算基于此实现 详见 [NOTICE](NOTICE) 和 [LICENSE](LICENSE)。 + +## Changelog + +### 1.4.0 (2026-05-28) + +**技术指标计算** — 集成 [MyTT](https://github.com/mpquant/MyTT) 麦语言指标库,支持 30 个常用技术指标,一步获取 K 线 + 指标值。 + +- 新增 `indicator.py` 核心模块:注册表驱动的指标调度,`compute_indicators()` 纯计算无 IO +- 新增 `MacClient.get_stock_kline_with_indicators()` / `AsyncMacClient` 同名方法 +- 新增 `UnifiedTdxClient.get_stock_kline_with_indicators()` / `AsyncUnifiedTdxClient` 同名方法 +- 新增 CLI 命令 `easy-tdx indicator` 和 `easy-tdx indicator-list` +- 自动获取 200+ 条历史数据预热 EMA,用户只需指定返回条数 +- 支持的指标:MACD, KDJ, RSI, BOLL, DMI, ATR, WR, CCI, BIAS, OBV, VR, EMV, MFI, BRAR, ASI, TRIX, DPO, MTM, ROC, EXPMA, BBI, PSY, DFMA, CR, KTN, XSII, MASS, TAQ + +### 1.3.1 (2025-05-15) + +- 新增 `board-summary` 和 `board-ranking` CLI 命令 +- 新增 `get_board_summary()` 板块汇总(成交额、主力净流入、涨跌家数) +- 新增 `get_board_ranking()` 板块涨跌幅排行榜 + +### 1.3.0 (2025-05-12) + +- 新增 MAC 协议客户端 `MacClient` / `AsyncMacClient`(端口 7709) +- 新增扩展市场客户端 `MacExClient` / `AsyncMacExClient`(端口 7727) +- 新增统一客户端 `UnifiedTdxClient` 自动路由 A 股 / 扩展市场 +- 新增板块、资金流向、集合竞价、异动、个股特征等数据接口 +- 新增 `easy-tdx` CLI 工具,默认 JSON 输出 + +### 1.2.1 (2025-04-20) + +- 离线数据读取模块(日线、分钟线、板块、财务) +- 除权除息、股本变迁读取 + +### 1.0.0 (2025-03-01) + +- 首个正式版本 +- TdxClient / AsyncTdxClient 标准协议客户端 +- K 线、实时报价、分时、逐笔成交、财务数据 diff --git a/examples/20_cli/cli_examples.sh b/examples/20_cli/cli_examples.sh index f5baacf..6d93689 100644 --- a/examples/20_cli/cli_examples.sh +++ b/examples/20_cli/cli_examples.sh @@ -313,3 +313,81 @@ echo "=== 23. 获取扩展市场分时图(港股腾讯)===" # 09:34:00 00:00 532.80 532.56 4100 # 09:35:00 00:00 533.20 532.84 3500 # ...(共约330条) + +echo "=== 24. 列出可用技术指标 ===" +# 列出所有支持的技术指标名称、输入需求和输出列。 +# easy-tdx indicator-list --table +# 输出: +# name description inputs outputs default_params +# MACD MACD 指数平滑异同移动平均线 ['close'] ['MACD_DIF', 'MACD_DEA', ...] {'SHORT': 12, 'LONG': 26, 'M': 9} +# KDJ KDJ 随机指标 ['close', 'high', ...] ['KDJ_K', 'KDJ_D', 'KDJ_J'] {'N': 9, 'M1': 3, 'M2': 3} +# RSI RSI 相对强弱指标 ['close'] ['RSI'] {'N': 24} +# BOLL BOLL 布林带 ['close'] ['BOLL_UPPER', 'BOLL_MID'...] {'N': 20, 'P': 2} +# ...(共30个指标) + +echo "=== 25. 计算单个技术指标(MACD)===" +# 计算单只股票的技术指标。默认前复权(QFQ),返回最近 30 条。 +# 参数: <指标名> -m <市场> -c <代码> --count N --table +# 返回列: datetime, open, high, low, close, vol, amount + 指标列 +# easy-tdx indicator MACD -m SH -c 600519 --table +# 输出: +# datetime open high low close vol amount MACD_DIF MACD_DEA MACD_HIST +# 2025-05-06 00:00:00 1498.00 1518.00 1492.00 1510.00 16540 2500000000 -4.56 -2.94 -3.24 +# 2025-05-07 00:00:00 1505.00 1516.00 1490.00 1498.00 14280 2150000000 -3.12 -3.18 0.11 +# 2025-05-08 00:00:00 1492.00 1510.00 1485.00 1505.00 15670 2350000000 -2.45 -3.03 1.16 +# 2025-05-09 00:00:00 1498.00 1516.00 1490.00 1498.00 14280 2150000000 -1.89 -2.80 1.82 +# 2025-05-12 00:00:00 1505.00 1516.00 1490.00 1498.00 14280 2150000000 -1.78 -2.60 1.64 +# ...(默认30条) + +echo "=== 26. 同时计算多个指标 ===" +# 用逗号分隔多个指标名称(不区分大小写)。 +# easy-tdx indicator MACD,KDJ,RSI,BOLL -m SH -c 600519 --count 5 --table +# 输出: +# datetime close MACD_DIF MACD_DEA MACD_HIST KDJ_K KDJ_D KDJ_J RSI BOLL_UPPER BOLL_MID BOLL_LOWER +# 2025-05-09 00:00 1505.00 -1.89 -2.80 1.82 45.23 52.34 31.01 55.6 1530.45 1500.12 1469.79 +# 2025-05-12 00:00 1498.00 -1.78 -2.60 1.64 38.56 48.89 17.90 48.2 1528.90 1498.56 1468.22 +# 2025-05-13 00:00 1510.00 -0.89 -2.26 2.74 62.34 52.17 82.68 56.8 1527.34 1497.00 1466.66 +# 2025-05-14 00:00 1509.00 -0.12 -1.83 3.42 58.12 53.56 67.24 52.3 1525.78 1495.44 1465.10 +# 2025-05-15 00:00 1521.00 1.23 -1.22 4.90 78.45 59.74 115.87 65.1 1524.22 1493.88 1463.54 + +echo "=== 27. 自定义指标参数 ===" +# 通过 --params 覆盖默认参数。格式: KEY=VALUE 或 INDICATOR.KEY=VALUE +# 修改 MACD 短周期为 10,长周期为 22 +# easy-tdx indicator MACD -m SH -c 600519 --params SHORT=10,LONG=22 --table +# +# 同时计算 MACD 和 KDJ,分别为它们设置不同参数: +# easy-tdx indicator MACD,KDJ -m SH -c 600519 --params MACD.SHORT=10,KDJ.N=14 --table + +echo "=== 28. 仅输出指标值(不含 OHLCV)===" +# 加 --no-ohlcv 隐藏原始 K 线列,仅显示时间 + 指标值。 +# easy-tdx indicator RSI -m SZ -c 000001 --no-ohlcv --count 5 --table +# 输出: +# datetime RSI +# 2025-05-09 00:00:00 52.34 +# 2025-05-12 00:00:00 48.67 +# 2025-05-13 00:00:00 56.12 +# 2025-05-14 00:00:00 51.89 +# 2025-05-15 00:00:00 63.45 + +echo "=== 29. 分钟 K 线技术指标 ===" +# 使用 --period 指定分钟周期,与 K 线命令相同。 +# easy-tdx indicator MACD -m SH -c 600519 --period 5MIN --count 10 --table +# 输出: +# datetime close MACD_DIF MACD_DEA MACD_HIST +# 2025-05-15 14:10 1520.50 0.34 0.28 0.12 +# 2025-05-15 14:15 1518.20 0.21 0.27 -0.11 +# 2025-05-15 14:20 1519.80 0.18 0.25 -0.15 +# 2025-05-15 14:25 1521.00 0.23 0.25 -0.04 +# ...(共10条) + +echo "=== 30. 常用指标快速参考 ===" +# MACD: easy-tdx indicator MACD -m SH -c 600519 --table +# KDJ: easy-tdx indicator KDJ -m SZ -c 000001 --table +# RSI: easy-tdx indicator RSI -m SH -c 600519 --table +# BOLL: easy-tdx indicator BOLL -m SH -c 600519 --table +# DMI: easy-tdx indicator DMI -m SH -c 600519 --table +# ATR: easy-tdx indicator ATR -m SH -c 600519 --table +# WR: easy-tdx indicator WR -m SH -c 600519 --table +# CCI: easy-tdx indicator CCI -m SH -c 600519 --table +# BIAS: easy-tdx indicator BIAS -m SZ -c 000001 --table +# OBV: easy-tdx indicator OBV -m SZ -c 000001 --table diff --git a/examples/21_indicator/basic_indicators.py b/examples/21_indicator/basic_indicators.py new file mode 100644 index 0000000..0378f31 --- /dev/null +++ b/examples/21_indicator/basic_indicators.py @@ -0,0 +1,142 @@ +"""演示:技术指标计算。 + +通过 MacClient 的 get_stock_kline_with_indicators() 获取 K 线并直接计算技术指标。 +内部自动获取 200+ 条历史数据进行 EMA 预热,仅返回最后 count 条结果。 + +也可单独使用 compute_indicators() 对已有的 K 线 DataFrame 计算指标。 + +支持的指标(30 个): + MACD KDJ RSI BOLL DMI ATR WR CCI BIAS OBV + VR EMV MFI BRAR ASI TRIX DPO MTM ROC EXPMA + BBI PSY DFMA CR KTN XSII MASS TAQ + +参数: + market -- 市场代码(Market.SH=1, Market.SZ=0) + code -- 股票代码 + indicators -- 指标名称列表(不区分大小写),如 ["MACD", "KDJ"] + count -- 返回条数(默认 30) + adjust -- 复权方式(默认 QFQ 前复权,技术分析推荐前复权) + params -- 可选参数覆盖,如 {"MACD": {"SHORT": 10}} + +返回 DataFrame 列说明(以 MACD 为例): + datetime datetime K 线时间 + open float 开盘价 + high float 最高价 + low float 最低价 + close float 收盘价 + vol float 成交量 + amount float 成交额 + MACD_DIF float MACD 的 DIF 线 + MACD_DEA float MACD 的 DEA 线 + MACD_HIST float MACD 柱状图((DIF-DEA)*2) +""" + +from easy_tdx import Adjust, MacClient, Market, Period + +with MacClient.from_best_host() as c: + # --- MACD(贵州茅台,日线,前复权)--- + print("=== MACD(贵州茅台 600519)===") + df = c.get_stock_kline_with_indicators( + Market.SH, + "600519", + indicators=["MACD"], + count=10, + ) + print(df[["datetime", "close", "MACD_DIF", "MACD_DEA", "MACD_HIST"]].to_string(index=False)) + + # --- KDJ(平安银行)--- + print("\n=== KDJ(平安银行 000001)===") + df = c.get_stock_kline_with_indicators( + Market.SZ, + "000001", + indicators=["KDJ"], + count=10, + ) + print(df[["datetime", "close", "KDJ_K", "KDJ_D", "KDJ_J"]].to_string(index=False)) + + # --- RSI(贵州茅台)--- + print("\n=== RSI(贵州茅台 600519,N=6 短周期)===") + df = c.get_stock_kline_with_indicators( + Market.SH, + "600519", + indicators=["RSI"], + count=10, + params={"RSI": {"N": 6}}, + ) + print(df[["datetime", "close", "RSI"]].to_string(index=False)) + + # --- BOLL 布林带 --- + print("\n=== BOLL 布林带(贵州茅台 600519)===") + df = c.get_stock_kline_with_indicators( + Market.SH, + "600519", + indicators=["BOLL"], + count=10, + ) + print(df[["datetime", "close", "BOLL_UPPER", "BOLL_MID", "BOLL_LOWER"]].to_string(index=False)) + + # --- 多指标同时计算 --- + print("\n=== MACD + KDJ + RSI + BOLL 联合计算 ===") + df = c.get_stock_kline_with_indicators( + Market.SH, + "600519", + indicators=["MACD", "KDJ", "RSI", "BOLL"], + count=5, + ) + cols = ["datetime", "close", "MACD_DIF", "KDJ_K", "RSI", "BOLL_UPPER", "BOLL_LOWER"] + print(df[cols].to_string(index=False)) + + # --- 仅输出指标列(不含 OHLCV)--- + print("\n=== 仅指标值(--no-ohlcv 模式)===") + df = c.get_stock_kline_with_indicators( + Market.SZ, + "000001", + indicators=["MACD", "RSI"], + count=5, + ) + indicator_cols = [ + c for c in df.columns if c not in ("open", "high", "low", "close", "vol", "amount") + ] + print(df[indicator_cols].to_string(index=False)) + + # --- 分钟 K 线 + 指标 --- + print("\n=== 5 分钟线 MACD(贵州茅台 600519)===") + df = c.get_stock_kline_with_indicators( + Market.SH, + "600519", + indicators=["MACD"], + period=Period.MIN_5, + count=5, + ) + print(df[["datetime", "close", "MACD_DIF", "MACD_DEA", "MACD_HIST"]].to_string(index=False)) + + # --- 使用 compute_indicators 独立计算 --- + print("\n=== 独立使用 compute_indicators ===") + from easy_tdx.indicator import compute_indicators + + raw_df = c.get_stock_kline(Market.SH, "600519", Period.DAILY, count=200, adjust=Adjust.QFQ) + result = compute_indicators(raw_df, ["ATR", "CCI", "WR"], tail=5) + print(result[["datetime", "close", "ATR", "CCI", "WR1", "WR2"]].to_string(index=False)) + +# 运行结果(示例): +# === MACD(贵州茅台 600519)=== +# datetime close MACD_DIF MACD_DEA MACD_HIST +# 2025-05-02 00:00:00 1492.00 -4.12 -1.56 -5.12 +# 2025-05-05 00:00:00 1485.00 -5.23 -2.29 -5.88 +# 2025-05-06 00:00:00 1498.00 -4.56 -2.94 -3.24 +# 2025-05-07 00:00:00 1510.00 -3.12 -3.18 0.11 +# 2025-05-08 00:00:00 1505.00 -2.45 -3.03 1.16 +# 2025-05-09 00:00:00 1505.00 -1.89 -2.80 1.82 +# 2025-05-12 00:00:00 1498.00 -1.78 -2.60 1.64 +# 2025-05-13 00:00:00 1510.00 -0.89 -2.26 2.74 +# 2025-05-14 00:00:00 1509.00 -0.12 -1.83 3.42 +# 2025-05-15 00:00:00 1521.00 1.23 -1.22 4.90 +# +# === KDJ(平安银行 000001)=== +# datetime close KDJ_K KDJ_D KDJ_J +# 2025-05-02 00:00:00 12.45 65.32 58.76 78.44 +# 2025-05-05 00:00:00 12.30 42.15 53.23 19.98 +# 2025-05-06 00:00:00 12.58 71.23 59.23 95.24 +# 2025-05-07 00:00:00 12.72 82.45 65.47 116.41 +# 2025-05-08 00:00:00 12.65 74.56 67.29 89.11 +# ... diff --git a/examples/21_indicator/list_indicators.py b/examples/21_indicator/list_indicators.py new file mode 100644 index 0000000..0e7a587 --- /dev/null +++ b/examples/21_indicator/list_indicators.py @@ -0,0 +1,50 @@ +"""演示:列出所有可用技术指标及其参数。 + +使用 list_indicators() 查看所有支持的指标名称、输入需求、输出列和默认参数。 +无需网络连接。 +""" + +from easy_tdx.indicator import list_indicators + +indicators = list_indicators() + +print(f"共 {len(indicators)} 个技术指标\n") + +# 按所需输入列分组展示 +groups: dict[str, list[dict]] = {} +for info in indicators: + key = "+".join(info["inputs"]) + groups.setdefault(key, []).append(info) + +for inputs, items in groups.items(): + print(f"── 输入: {inputs} {'─' * 50}") + for item in items: + params_str = ( + ", ".join(f"{k}={v}" for k, v in item["default_params"].items()) + if item["default_params"] + else "" + ) + outputs_str = ", ".join(item["outputs"]) + line = f" {item['name']:<8} {item['description']}" + if params_str: + line += f" (默认: {params_str})" + print(line) + print(f" 输出: {outputs_str}") + print() + +# 运行结果: +# 共 30 个技术指标 +# +# ── 输入: close ────────────────────────────────────────────────────────── +# MACD MACD 指数平滑异同移动平均线 (默认: SHORT=12, LONG=26, M=9) +# 输出: MACD_DIF, MACD_DEA, MACD_HIST +# RSI RSI 相对强弱指标 (默认: N=24) +# 输出: RSI +# BOLL BOLL 布林带 (默认: N=20, P=2) +# 输出: BOLL_UPPER, BOLL_MID, BOLL_LOWER +# ... +# +# ── 输入: close+high+low ──────────────────────────────────────────────── +# KDJ KDJ 随机指标 (默认: N=9, M1=3, M2=3) +# 输出: KDJ_K, KDJ_D, KDJ_J +# ... diff --git a/pyproject.toml b/pyproject.toml index d1b0081..6013958 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "easy-tdx" -version = "1.3.1" +version = "1.4.0" description = "通达信 TCP 协议行情数据客户端,支持在线行情与离线本地数据读取" readme = "README.md" requires-python = ">=3.10" diff --git a/src/easy_tdx/MyTT.py b/src/easy_tdx/MyTT.py new file mode 100644 index 0000000..e105d3d --- /dev/null +++ b/src/easy_tdx/MyTT.py @@ -0,0 +1,308 @@ +# MyTT 麦语言-通达信-同花顺指标实现 https://github.com/mpquant/MyTT +# MyTT高级函数验证版本: https://github.com/mpquant/MyTT/blob/main/MyTT_plus.py +# Python2老版本pandas特别的MyTT: https://github.com/mpquant/MyTT/blob/main/MyTT_python2.py +# V2.1 2021-6-6 新增 BARSLAST函数 SLOPE,FORCAST线性回归预测函数 +# V2.3 2021-6-13 新增 TRIX,DPO,BRAR,DMA,MTM,MASS,ROC,VR,ASI等指标 +# V2.4 2021-6-27 新增 EXPMA,OBV,MFI指标, 改进SMA核心函数(核心函数彻底无循环) +# V2.7 2021-11-21 修正 SLOPE,BARSLAST,函数,新加FILTER,LONGCROSS, 感谢qzhjiang对SLOPE,SMA等函数的指正 +# V2.8 2021-11-23 修正 FORCAST,WMA函数,欢迎qzhjiang,stanene,bcq加入社群,一起来完善myTT库 +# V2.9 2021-11-29 新增 HHVBARS,LLVBARS,CONST, VALUEWHEN功能函数 +# V2.92 2021-11-30 新增 BARSSINCEN函数,现在可以 pip install MyTT 完成安装 +# V3.0 2021-12-04 改进 DMA函数支持序列,新增XS2 薛斯通道II指标 +# V3.1 2021-12-19 新增 TOPRANGE,LOWRANGE一级函数 +# V3.2 2023-04-04 新增 CR指标 +# V3.3 2023-11-09 新增 SIN,COS,TAN序列处理的三角函数 + + +#以下所有函数如无特别说明,输入参数S均为numpy序列或者列表list,N为整型int +#应用层1级函数完美兼容通达信或同花顺,具体使用方法请参考通达信 + +import numpy as np; import pandas as pd + +#------------------ 0级:核心工具函数 -------------------------------------------- +def RD(N,D=3): return np.round(N,D) #四舍五入取3位小数 +def RET(S,N=1): return np.array(S)[-N] #返回序列倒数第N个值,默认返回最后一个 +def ABS(S): return np.abs(S) #返回N的绝对值 +def LN(S): return np.log(S) #求底是e的自然对数, +def POW(S,N): return np.power(S,N) #求S的N次方 +def SQRT(S): return np.sqrt(S) #求S的平方根 +def SIN(S): return np.sin(S) #求S的正弦值(弧度) +def COS(S): return np.cos(S) #求S的余弦值(弧度) +def TAN(S): return np.tan(S) #求S的正切值(弧度) +def MAX(S1,S2): return np.maximum(S1,S2) #序列max +def MIN(S1,S2): return np.minimum(S1,S2) #序列min +def IF(S,A,B): return np.where(S,A,B) #序列布尔判断 return=A if S==True else B + + +def REF(S, N=1): #对序列整体下移动N,返回序列(shift后会产生NAN) + return pd.Series(S).shift(N).values + +def DIFF(S, N=1): #前一个值减后一个值,前面会产生nan + return pd.Series(S).diff(N).values #np.diff(S)直接删除nan,会少一行 + +def STD(S,N): #求序列的N日标准差,返回序列 + return pd.Series(S).rolling(N).std(ddof=0).values + +def SUM(S, N): #对序列求N天累计和,返回序列 N=0对序列所有依次求和 + return pd.Series(S).rolling(N).sum().values if N>0 else pd.Series(S).cumsum().values + +def CONST(S): #返回序列S最后的值组成常量序列 + return np.full(len(S),S[-1]) + +def HHV(S,N): #HHV(C, 5) 最近5天收盘最高价 + return pd.Series(S).rolling(N).max().values + +def LLV(S,N): #LLV(C, 5) 最近5天收盘最低价 + return pd.Series(S).rolling(N).min().values + +def HHVBARS(S,N): #求N周期内S最高值到当前周期数, 返回序列 + return pd.Series(S).rolling(N).apply(lambda x: np.argmax(x[::-1]),raw=True).values + +def LLVBARS(S,N): #求N周期内S最低值到当前周期数, 返回序列 + return pd.Series(S).rolling(N).apply(lambda x: np.argmin(x[::-1]),raw=True).values + +def MA(S,N): #求序列的N日简单移动平均值,返回序列 + return pd.Series(S).rolling(N).mean().values + +def EMA(S,N): #指数移动平均,为了精度 S>4*N EMA至少需要120周期 alpha=2/(span+1) + return pd.Series(S).ewm(span=N, adjust=False).mean().values + +def SMA(S, N, M=1): #中国式的SMA,至少需要120周期才精确 (雪球180周期) alpha=1/(1+com) + return pd.Series(S).ewm(alpha=M/N,adjust=False).mean().values #com=N-M/M + +def WMA(S, N): #通达信S序列的N日加权移动平均 Yn = (1*X1+2*X2+3*X3+...+n*Xn)/(1+2+3+...+Xn) + return pd.Series(S).rolling(N).apply(lambda x:x[::-1].cumsum().sum()*2/N/(N+1),raw=True).values + +def DMA(S, A): #求S的动态移动平均,A作平滑因子,必须 0B & A>0 & B>=0 + return np.array(pd.Series(S).rolling(A+1).apply(lambda x:np.all(x[::-1][B:]),raw=True),dtype=bool) + +#------------------ 1级:应用层函数(通过0级核心函数实现)使用方法请参考通达信-------------------------------- +def COUNT(S, N): # COUNT(CLOSE>O, N): 最近N天满足S_BOO的天数 True的天数 + return SUM(S,N) + +def EVERY(S, N): # EVERY(CLOSE>O, 5) 最近N天是否都是True + return IF(SUM(S,N)==N,True,False) + +def EXIST(S, N): # EXIST(CLOSE>3010, N=5) n日内是否存在一天大于3000点 + return IF(SUM(S,N)>0,True,False) + +def FILTER(S, N): # FILTER函数,S满足条件后,将其后N周期内的数据置为0, FILTER(C==H,5) + for i in range(len(S)): S[i+1:i+1+N]=0 if S[i] else S[i+1:i+1+N] + return S # 例:FILTER(C==H,5) 涨停后,后5天不再发出信号 + +def BARSLAST(S): #上一次条件成立到当前的周期, BARSLAST(C/REF(C,1)>=1.1) 上一次涨停到今天的天数 + M=np.concatenate(([0],np.where(S,1,0))) + for i in range(1, len(M)): M[i]=0 if M[i] else M[i-1]+1 + return M[1:] + +def BARSLASTCOUNT(S): # 统计连续满足S条件的周期数 by jqz1226 + rt = np.zeros(len(S)+1) # BARSLASTCOUNT(CLOSE>OPEN)表示统计连续收阳的周期数 + for i in range(len(S)): rt[i+1]=rt[i]+1 if S[i] else rt[i+1] + return rt[1:] + +def BARSSINCEN(S, N): # N周期内第一次S条件成立到现在的周期数,N为常量 by jqz1226 + return pd.Series(S).rolling(N).apply(lambda x:N-1-np.argmax(x) if np.argmax(x) or x[0] else 0,raw=True).fillna(0).values.astype(int) + +def CROSS(S1, S2): # 判断向上金叉穿越 CROSS(MA(C,5),MA(C,10)) 判断向下死叉穿越 CROSS(MA(C,10),MA(C,5)) + return np.concatenate(([False], np.logical_not((S1>S2)[:-1]) & (S1>S2)[1:])) # 不使用0级函数,移植方便 by jqz1226 + +def LONGCROSS(S1,S2,N): # 两条线维持一定周期后交叉,S1在N周期内都小于S2,本周期从S1下方向上穿过S2时返回1,否则返回0 + return np.array(np.logical_and(LAST(S1S2)),dtype=bool) # N=1时等同于CROSS(S1, S2) + +def VALUEWHEN(S, X): # 当S条件成立时,取X的当前值,否则取VALUEWHEN的上个成立时的X值 by jqz1226 + return pd.Series(np.where(S,X,np.nan)).ffill().values + +def BETWEEN(S, A, B): # S处于A和B之间时为真。 包括 AS>B + return ((AS) & (S>B)) + +def TOPRANGE(S): # TOPRANGE(HIGH)表示当前最高价是近多少周期内最高价的最大值 by jqz1226 + rt = np.zeros(len(S)) + for i in range(1,len(S)): rt[i] = np.argmin(np.flipud(S[:i]S[i])) + return rt.astype('int') + + +#------------------ 2级:技术指标函数(全部通过0级,1级函数实现) ------------------------------ +def MACD(CLOSE,SHORT=12,LONG=26,M=9): # EMA的关系,S取120日,和雪球小数点2位相同 + DIF = EMA(CLOSE,SHORT)-EMA(CLOSE,LONG); + DEA = EMA(DIF,M); MACD=(DIF-DEA)*2 + return RD(DIF),RD(DEA),RD(MACD) + +def KDJ(CLOSE,HIGH,LOW, N=9,M1=3,M2=3): # KDJ指标 + low_n = LLV(LOW, N) + high_n = HHV(HIGH, N) + high_low_diff = high_n - low_n + # 避免除零:当最高价等于最低价时,RSV 应该为 50(中性) + with np.errstate(divide='ignore', invalid='ignore'): + rsv = (CLOSE - low_n) / high_low_diff * 100 + rsv = np.where(high_low_diff == 0, 50, rsv) # 除零时返回 50 + K = EMA(rsv, (M1*2-1)); D = EMA(K,(M2*2-1)); J=K*3-D*2 + return K, D, J + +def RSI(CLOSE, N=24): # RSI指标,和通达信小数点2位相同 + DIF = CLOSE-REF(CLOSE,1) + abs_dif_sma = SMA(ABS(DIF), N) + # 避免除零:当价格完全不变时,RSI 应该为 50(中性) + with np.errstate(divide='ignore', invalid='ignore'): + rsi_value = SMA(MAX(DIF,0), N) / abs_dif_sma * 100 + rsi_value = np.where(abs_dif_sma == 0, 50, rsi_value) # 除零时返回 50 + return RD(rsi_value) + +def WR(CLOSE, HIGH, LOW, N=10, N1=6): #W&R 威廉指标 + high_n = HHV(HIGH, N) + low_n = LLV(LOW, N) + high_low_diff = high_n - low_n + with np.errstate(divide='ignore', invalid='ignore'): + wr = (high_n - CLOSE) / high_low_diff * 100 + wr = np.where(high_low_diff == 0, 50, wr) # 除零时返回 50 + + high_n1 = HHV(HIGH, N1) + low_n1 = LLV(LOW, N1) + high_low_diff1 = high_n1 - low_n1 + with np.errstate(divide='ignore', invalid='ignore'): + wr1 = (high_n1 - CLOSE) / high_low_diff1 * 100 + wr1 = np.where(high_low_diff1 == 0, 50, wr1) # 除零时返回 50 + + return RD(wr), RD(wr1) + +def BIAS(CLOSE,L1=6, L2=12, L3=24): # BIAS乖离率 + BIAS1 = (CLOSE - MA(CLOSE, L1)) / MA(CLOSE, L1) * 100 + BIAS2 = (CLOSE - MA(CLOSE, L2)) / MA(CLOSE, L2) * 100 + BIAS3 = (CLOSE - MA(CLOSE, L3)) / MA(CLOSE, L3) * 100 + return RD(BIAS1), RD(BIAS2), RD(BIAS3) + +def BOLL(CLOSE,N=20, P=2): #BOLL指标,布林带 + MID = MA(CLOSE, N); + UPPER = MID + STD(CLOSE, N) * P + LOWER = MID - STD(CLOSE, N) * P + return RD(UPPER), RD(MID), RD(LOWER) + +def PSY(CLOSE,N=12, M=6): + PSY=COUNT(CLOSE>REF(CLOSE,1),N)/N*100 + PSYMA=MA(PSY,M) + return RD(PSY),RD(PSYMA) + +def CCI(CLOSE,HIGH,LOW,N=14): + TP=(HIGH+LOW+CLOSE)/3 + return (TP-MA(TP,N))/(0.015*AVEDEV(TP,N)) + +def ATR(CLOSE,HIGH,LOW, N=20): #真实波动N日平均值 + TR = MAX(MAX((HIGH - LOW), ABS(REF(CLOSE, 1) - HIGH)), ABS(REF(CLOSE, 1) - LOW)) + return MA(TR, N) + +def BBI(CLOSE,M1=3,M2=6,M3=12,M4=20): #BBI多空指标 + return (MA(CLOSE,M1)+MA(CLOSE,M2)+MA(CLOSE,M3)+MA(CLOSE,M4))/4 + +def DMI(CLOSE,HIGH,LOW,M1=14,M2=6): #动向指标:结果和同花顺,通达信完全一致 + TR = SUM(MAX(MAX(HIGH - LOW, ABS(HIGH - REF(CLOSE, 1))), ABS(LOW - REF(CLOSE, 1))), M1) + HD = HIGH - REF(HIGH, 1); LD = REF(LOW, 1) - LOW + DMP = SUM(IF((HD > 0) & (HD > LD), HD, 0), M1) + DMM = SUM(IF((LD > 0) & (LD > HD), LD, 0), M1) + PDI = DMP * 100 / TR; MDI = DMM * 100 / TR + ADX = MA(ABS(MDI - PDI) / (PDI + MDI) * 100, M2) + ADXR = (ADX + REF(ADX, M2)) / 2 + return PDI, MDI, ADX, ADXR + +def TAQ(HIGH,LOW,N): #唐安奇通道(海龟)交易指标,大道至简,能穿越牛熊 + UP=HHV(HIGH,N); DOWN=LLV(LOW,N); MID=(UP+DOWN)/2 + return UP,MID,DOWN + +def KTN(CLOSE,HIGH,LOW,N=20,M=10): #肯特纳交易通道, N选20日,ATR选10日 + MID=EMA((HIGH+LOW+CLOSE)/3,N) + ATRN=ATR(CLOSE,HIGH,LOW,M) + UPPER=MID+2*ATRN; LOWER=MID-2*ATRN + return UPPER,MID,LOWER + +def TRIX(CLOSE,M1=12, M2=20): #三重指数平滑平均线 + TR = EMA(EMA(EMA(CLOSE, M1), M1), M1) + TRIX = (TR - REF(TR, 1)) / REF(TR, 1) * 100 + TRMA = MA(TRIX, M2) + return TRIX, TRMA + +def VR(CLOSE,VOL,M1=26): #VR容量比率 + LC = REF(CLOSE, 1) + return SUM(IF(CLOSE > LC, VOL, 0), M1) / SUM(IF(CLOSE <= LC, VOL, 0), M1) * 100 + +def CR(CLOSE,HIGH,LOW,N=20): #CR价格动量指标 + MID=REF(HIGH+LOW+CLOSE,1)/3; + return SUM(MAX(0,HIGH-MID),N)/SUM(MAX(0,MID-LOW),N)*100 + +def EMV(HIGH,LOW,VOL,N=14,M=9): #简易波动指标 + VOLUME=MA(VOL,N)/VOL; MID=100*(HIGH+LOW-REF(HIGH+LOW,1))/(HIGH+LOW) + EMV=MA(MID*VOLUME*(HIGH-LOW)/MA(HIGH-LOW,N),N); MAEMV=MA(EMV,M) + return EMV,MAEMV + + +def DPO(CLOSE,M1=20, M2=10, M3=6): #区间震荡线 + DPO = CLOSE - REF(MA(CLOSE, M1), M2); MADPO = MA(DPO, M3) + return DPO, MADPO + +def BRAR(OPEN,CLOSE,HIGH,LOW,M1=26): #BRAR-ARBR 情绪指标 + AR = SUM(HIGH - OPEN, M1) / SUM(OPEN - LOW, M1) * 100 + BR = SUM(MAX(0, HIGH - REF(CLOSE, 1)), M1) / SUM(MAX(0, REF(CLOSE, 1) - LOW), M1) * 100 + return AR, BR + +def DFMA(CLOSE,N1=10,N2=50,M=10): #平行线差指标 + DIF=MA(CLOSE,N1)-MA(CLOSE,N2); DIFMA=MA(DIF,M) #通达信指标叫DMA 同花顺叫新DMA + return DIF,DIFMA + +def MTM(CLOSE,N=12,M=6): #动量指标 + MTM=CLOSE-REF(CLOSE,N); MTMMA=MA(MTM,M) + return MTM,MTMMA + +def MASS(HIGH,LOW,N1=9,N2=25,M=6): #梅斯线 + MASS=SUM(MA(HIGH-LOW,N1)/MA(MA(HIGH-LOW,N1),N1),N2) + MA_MASS=MA(MASS,M) + return MASS,MA_MASS + +def ROC(CLOSE,N=12,M=6): #变动率指标 + ROC=100*(CLOSE-REF(CLOSE,N))/REF(CLOSE,N); MAROC=MA(ROC,M) + return ROC,MAROC + +def EXPMA(CLOSE,N1=12,N2=50): #EMA指数平均数指标 + return EMA(CLOSE,N1),EMA(CLOSE,N2); + +def OBV(CLOSE,VOL): #能量潮指标 + return SUM(IF(CLOSE>REF(CLOSE,1),VOL,IF(CLOSEREF(TYP,1),TYP*VOL,0),N)/SUM(IF(TYPBB) & (AA>CC),AA+BB/2+DD/4,IF( (BB>CC) & (BB>AA),BB+AA/2+DD/4,CC+DD/4)); + X=(CLOSE-LC+(CLOSE-OPEN)/2+LC-REF(OPEN,1)); + SI=16*X/R*MAX(AA,BB); ASI=SUM(SI,M1); ASIT=MA(ASI,M2); + return ASI,ASIT + +def XSII(CLOSE, HIGH, LOW, N=102, M=7): #薛斯通道II + AA = MA((2*CLOSE + HIGH + LOW)/4, 5) #最新版DMA才支持 2021-12-4 + TD1 = AA*N/100; TD2 = AA*(200-N) / 100 + CC = ABS((2*CLOSE + HIGH + LOW)/4 - MA(CLOSE,20))/MA(CLOSE,20) + DD = DMA(CLOSE,CC); TD3=(1+M/100)*DD; TD4=(1-M/100)*DD + return TD1, TD2, TD3, TD4 + + + #望大家能提交更多指标和函数 https://github.com/mpquant/MyTT diff --git a/src/easy_tdx/__init__.py b/src/easy_tdx/__init__.py index 713b532..b83b87e 100644 --- a/src/easy_tdx/__init__.py +++ b/src/easy_tdx/__init__.py @@ -107,4 +107,4 @@ __all__ = [ "save_best_ex_host", ] -__version__ = "1.3.0" +__version__ = "1.4.0" diff --git a/src/easy_tdx/cli/__init__.py b/src/easy_tdx/cli/__init__.py index b8337b3..14c8008 100644 --- a/src/easy_tdx/cli/__init__.py +++ b/src/easy_tdx/cli/__init__.py @@ -11,6 +11,7 @@ from .cmd_capital import capital_flow from .cmd_ex import ex from .cmd_finance import f10, fund_flow from .cmd_info import server_info, symbol_info +from .cmd_indicator import indicator, indicator_list from .cmd_kline import kline from .cmd_monitor import market_stat, unusual from .cmd_quote import quote, quote_list @@ -19,7 +20,7 @@ from .cmd_transaction import transaction @click.group() -@click.version_option(version="1.3.1", prog_name="easy-tdx") +@click.version_option(version="1.4.0", prog_name="easy-tdx") def cli() -> None: """easy-tdx -- 通达信行情数据 CLI(默认 JSON 输出,适合 Agent 使用)。 @@ -59,3 +60,5 @@ cli.add_command(symbol_info) cli.add_command(f10) cli.add_command(fund_flow) cli.add_command(ex) +cli.add_command(indicator) +cli.add_command(indicator_list) diff --git a/src/easy_tdx/cli/cmd_indicator.py b/src/easy_tdx/cli/cmd_indicator.py new file mode 100644 index 0000000..70311e2 --- /dev/null +++ b/src/easy_tdx/cli/cmd_indicator.py @@ -0,0 +1,131 @@ +"""技术指标命令。""" + +from __future__ import annotations + +import click + + +def _parse_indicator_params(s: str) -> dict[str, dict[str, int | float]]: + """解析指标参数字符串。 + + 格式: ``SHORT=10,LONG=22`` 或 ``MACD.SHORT=10,KDJ.N=14`` + 无前缀的参数应用到所有请求的指标。 + """ + result: dict[str, dict[str, int | float]] = {} + if not s: + return result + + for pair in s.split(","): + pair = pair.strip() + if "=" not in pair: + continue + key, val = pair.split("=", 1) + key = key.strip() + val = val.strip() + + if "." in key: + indicator, param = key.split(".", 1) + indicator = indicator.strip().upper() + param = param.strip() + result.setdefault(indicator, {})[param] = float(val) if "." in val else int(val) + else: + result.setdefault("*", {})[key] = float(val) if "." in val else int(val) + return result + + +@click.command() +@click.argument("indicators") +@click.option("--market", "-m", required=True, help="市场: SH/SZ/BJ") +@click.option("--code", "-c", required=True, help="股票代码") +@click.option( + "--period", + default="DAILY", + help="K线周期: DAILY/5MIN/15MIN/30MIN/60MIN/1MIN/WEEKLY/MONTHLY", +) +@click.option("--count", default=30, type=int, help="返回条数(默认30)") +@click.option("--adjust", default="QFQ", help="复权: NONE/QFQ/HFQ(默认QFQ)") +@click.option("--params", default=None, help="指标参数: SHORT=10,LONG=22 或 MACD.SHORT=10") +@click.option("--no-ohlcv", is_flag=True, help="不显示原始OHLCV列") +@click.option("--table", "use_table", is_flag=True, help="表格输出") +@click.option("--output", "output_fmt", type=click.Choice(["json", "table", "csv"]), default="json") +def indicator( + indicators: str, + market: str, + code: str, + period: str, + count: int, + adjust: str, + params: str | None, + no_ohlcv: bool, + use_table: bool, + output_fmt: str, +) -> None: + """计算技术指标。 + + 示例: + + easy-tdx indicator MACD -m SH -c 600519 --table + + easy-tdx indicator MACD,KDJ,RSI -m SH -c 600519 --count 10 --table + + easy-tdx indicator BOLL -m SZ -c 000001 --params N=10,P=1.5 + """ + from ..indicator import compute_indicators + from .conn import get_mac_client + from .output import print_error, print_output + from .parsers import parse_adjust, parse_market, parse_period + + fmt = "table" if use_table else output_fmt + mkt = parse_market(market) + indicator_list = [n.strip() for n in indicators.split(",")] + parsed_params = _parse_indicator_params(params) if params else {} + + # 将通配符参数应用到所有指标 + wildcard = parsed_params.pop("*", {}) + final_params: dict[str, dict[str, int | float]] = {} + for name in indicator_list: + final_params[name.upper()] = {**wildcard, **parsed_params.get(name.upper(), {})} + + fetch_count = max(120 + count, 200) + try: + with get_mac_client() as client: + df = client.get_stock_kline( + mkt, + code, + period=parse_period(period), + count=fetch_count, + adjust=parse_adjust(adjust), + ) + if df.empty: + print_error("未获取到K线数据") + return + result = compute_indicators( + df, + indicator_list, + final_params, + keep_ohlcv=not no_ohlcv, + tail=count, + ) + print_output(result, fmt) + except ValueError as e: + print_error(str(e)) + except Exception as e: + print_error(f"{type(e).__name__}: {e}") + + +@click.command("indicator-list") +@click.option("--table", "use_table", is_flag=True, help="表格输出") +@click.option("--output", "output_fmt", type=click.Choice(["json", "table", "csv"]), default="json") +def indicator_list(use_table: bool, output_fmt: str) -> None: + """列出可用的技术指标。""" + import pandas as pd + + from ..indicator import list_indicators + from .output import print_output + + fmt = "table" if use_table else output_fmt + info = list_indicators() + df = pd.DataFrame(info) + if fmt == "table": + df["default_params"] = df["default_params"].apply(lambda d: str(d)) + print_output(df, fmt) diff --git a/src/easy_tdx/indicator.py b/src/easy_tdx/indicator.py new file mode 100644 index 0000000..28bdb20 --- /dev/null +++ b/src/easy_tdx/indicator.py @@ -0,0 +1,277 @@ +"""技术指标计算模块 — 基于 MyTT 的纯计算层(无 IO)。""" + +from __future__ import annotations + +import warnings +from collections.abc import Callable +from dataclasses import dataclass + +import numpy as np +import pandas as pd + +from . import MyTT + + +@dataclass(frozen=True) +class IndicatorSpec: + """单个技术指标的元数据。""" + + name: str + inputs: tuple[str, ...] + outputs: tuple[str, ...] + func: Callable[..., object] + default_params: dict[str, int | float] + description: str + + +_REGISTRY: dict[str, IndicatorSpec] = {} + + +def _reg( + name: str, + inputs: tuple[str, ...], + outputs: tuple[str, ...], + func: Callable[..., object], + defaults: dict[str, int | float], + desc: str, +) -> None: + _REGISTRY[name.upper()] = IndicatorSpec( + name=name.upper(), + inputs=inputs, + outputs=outputs, + func=func, + default_params=defaults, + description=desc, + ) + + +# ── 仅需 close ────────────────────────────────────────────────────────── +_reg( + "MACD", + ("close",), + ("MACD_DIF", "MACD_DEA", "MACD_HIST"), + MyTT.MACD, + {"SHORT": 12, "LONG": 26, "M": 9}, + "MACD 指数平滑异同移动平均线", +) +_reg("RSI", ("close",), ("RSI",), MyTT.RSI, {"N": 24}, "RSI 相对强弱指标") +_reg( + "BOLL", + ("close",), + ("BOLL_UPPER", "BOLL_MID", "BOLL_LOWER"), + MyTT.BOLL, + {"N": 20, "P": 2}, + "BOLL 布林带", +) +_reg( + "BIAS", + ("close",), + ("BIAS1", "BIAS2", "BIAS3"), + MyTT.BIAS, + {"L1": 6, "L2": 12, "L3": 24}, + "BIAS 乖离率", +) +_reg("PSY", ("close",), ("PSY", "PSY_MA"), MyTT.PSY, {"N": 12, "M": 6}, "PSY 心理线") +_reg( + "TRIX", + ("close",), + ("TRIX", "TRIX_MA"), + MyTT.TRIX, + {"M1": 12, "M2": 20}, + "TRIX 三重指数平滑平均线", +) +_reg( + "DPO", ("close",), ("DPO", "DPO_MA"), MyTT.DPO, {"M1": 20, "M2": 10, "M3": 6}, "DPO 区间震荡线" +) +_reg("MTM", ("close",), ("MTM", "MTM_MA"), MyTT.MTM, {"N": 12, "M": 6}, "MTM 动量指标") +_reg("ROC", ("close",), ("ROC", "ROC_MA"), MyTT.ROC, {"N": 12, "M": 6}, "ROC 变动率指标") +_reg( + "EXPMA", + ("close",), + ("EXPMA_12", "EXPMA_50"), + MyTT.EXPMA, + {"N1": 12, "N2": 50}, + "EXPMA 指数平均数指标", +) +_reg("BBI", ("close",), ("BBI",), MyTT.BBI, {"M1": 3, "M2": 6, "M3": 12, "M4": 20}, "BBI 多空指标") +_reg( + "DFMA", + ("close",), + ("DFMA_DIF", "DFMA_DMA"), + MyTT.DFMA, + {"N1": 10, "N2": 50, "M": 10}, + "DFMA 平行线差指标", +) + +# ── 需要 close + high + low ───────────────────────────────────────────── +_reg( + "KDJ", + ("close", "high", "low"), + ("KDJ_K", "KDJ_D", "KDJ_J"), + MyTT.KDJ, + {"N": 9, "M1": 3, "M2": 3}, + "KDJ 随机指标", +) +_reg( + "DMI", + ("close", "high", "low"), + ("DMI_PDI", "DMI_MDI", "DMI_ADX", "DMI_ADXR"), + MyTT.DMI, + {"M1": 14, "M2": 6}, + "DMI 动向指标", +) +_reg("ATR", ("close", "high", "low"), ("ATR",), MyTT.ATR, {"N": 20}, "ATR 真实波幅均值") +_reg("WR", ("close", "high", "low"), ("WR1", "WR2"), MyTT.WR, {"N": 10, "N1": 6}, "WR 威廉指标") +_reg("CCI", ("close", "high", "low"), ("CCI",), MyTT.CCI, {"N": 14}, "CCI 顺势指标") +_reg("CR", ("close", "high", "low"), ("CR",), MyTT.CR, {"N": 20}, "CR 价格动量指标") +_reg( + "KTN", + ("close", "high", "low"), + ("KTN_UPPER", "KTN_MID", "KTN_LOWER"), + MyTT.KTN, + {"N": 20, "M": 10}, + "KTN 肯特纳通道", +) +_reg( + "XSII", + ("close", "high", "low"), + ("XSII_TD1", "XSII_TD2", "XSII_TD3", "XSII_TD4"), + MyTT.XSII, + {"N": 102, "M": 7}, + "XSII 薛斯通道II", +) + +# ── 需要 close + vol ──────────────────────────────────────────────────── +_reg("OBV", ("close", "vol"), ("OBV",), MyTT.OBV, {}, "OBV 能量潮指标") +_reg("VR", ("close", "vol"), ("VR",), MyTT.VR, {"M1": 26}, "VR 容量比率") + +# ── 需要 high + low + vol ─────────────────────────────────────────────── +_reg( + "EMV", + ("high", "low", "vol"), + ("EMV", "EMV_MA"), + MyTT.EMV, + {"N": 14, "M": 9}, + "EMV 简易波动指标", +) +_reg( + "MASS", + ("high", "low"), + ("MASS", "MASS_MA"), + MyTT.MASS, + {"N1": 9, "N2": 25, "M": 6}, + "MASS 梅斯线", +) + +# ── 需要 close + high + low + vol ────────────────────────────────────── +_reg("MFI", ("close", "high", "low", "vol"), ("MFI",), MyTT.MFI, {"N": 14}, "MFI 资金流量指标") + +# ── 需要 open + close + high + low ───────────────────────────────────── +_reg("BRAR", ("open", "close", "high", "low"), ("AR", "BR"), MyTT.BRAR, {"M1": 26}, "BRAR 情绪指标") +_reg( + "ASI", + ("open", "close", "high", "low"), + ("ASI", "ASI_MA"), + MyTT.ASI, + {"M1": 26, "M2": 10}, + "ASI 振动升降指标", +) + +# ── 仅需 high + low ──────────────────────────────────────────────────── +_reg( + "TAQ", ("high", "low"), ("TAQ_UP", "TAQ_MID", "TAQ_DOWN"), MyTT.TAQ, {"N": 20}, "TAQ 唐安奇通道" +) + + +def list_indicators() -> list[dict[str, object]]: + """返回所有可用指标的元数据。""" + return [ + { + "name": spec.name, + "description": spec.description, + "inputs": list(spec.inputs), + "outputs": list(spec.outputs), + "default_params": dict(spec.default_params), + } + for spec in _REGISTRY.values() + ] + + +def compute_indicators( + df: pd.DataFrame, + indicators: list[str], + params: dict[str, dict[str, int | float]] | None = None, + keep_ohlcv: bool = True, + tail: int | None = None, +) -> pd.DataFrame: + """在 K 线 DataFrame 上计算指定技术指标。 + + Args: + df: K 线数据,需包含 open/close/high/low/vol 等列。 + indicators: 指标名称列表(不区分大小写),如 ``["MACD", "KDJ"]``。 + params: 可选参数覆盖,如 ``{"MACD": {"SHORT": 10}}``。 + keep_ohlcv: True 则保留原始 OHLCV 列。 + tail: 计算后仅保留最后 N 行。 + + Returns: + 包含指标列的 DataFrame。 + """ + if df.empty: + return pd.DataFrame(df.copy()) + + params = params or {} + result_parts: list[pd.DataFrame] = [] + required_inputs: set[str] = set() + + names_upper = [n.strip().upper() for n in indicators] + unknown = [n for n in names_upper if n not in _REGISTRY] + if unknown: + raise ValueError(f"未知指标: {unknown}。可用指标: {sorted(_REGISTRY.keys())}") + + for name in names_upper: + spec = _REGISTRY[name] + required_inputs.update(spec.inputs) + + missing_cols = required_inputs - set(df.columns) + if missing_cols: + raise ValueError(f"DataFrame 缺少必要列: {missing_cols}。指标需要这些列: {required_inputs}") + + if len(df) < 120: + warnings.warn( + f"数据仅 {len(df)} 行,EMA 类指标至少需要 120 行才能精确收敛", + stacklevel=2, + ) + + for name in names_upper: + spec = _REGISTRY[name] + inputs = tuple(df[col].values for col in spec.inputs) + override = params.get(name, params.get(spec.name, {})) + kwargs = {**spec.default_params, **override} + raw = spec.func(*inputs, **kwargs) + + if isinstance(raw, tuple): + arrays = raw + else: + arrays = (raw,) + + if len(arrays) != len(spec.outputs): + raise RuntimeError(f"{name}: 预期 {len(spec.outputs)} 个输出,实际 {len(arrays)} 个") + + part = pd.DataFrame( + {col: arr for col, arr in zip(spec.outputs, arrays)}, + index=df.index, + ) + result_parts.append(part) + + indicator_df: pd.DataFrame = pd.concat(result_parts, axis=1) + + if keep_ohlcv: + out: pd.DataFrame = pd.concat([df, indicator_df], axis=1) + else: + time_cols = [c for c in ("datetime", "date") if c in df.columns] + out = pd.concat([df[time_cols], indicator_df], axis=1) if time_cols else indicator_df + + if tail is not None and tail > 0: + out = out.iloc[-tail:] + + return pd.DataFrame(out.reset_index(drop=True)) diff --git a/src/easy_tdx/mac/client.py b/src/easy_tdx/mac/client.py index e91c6f3..e59497f 100644 --- a/src/easy_tdx/mac/client.py +++ b/src/easy_tdx/mac/client.py @@ -375,6 +375,37 @@ class MacClient: return _to_df(all_bars) + def get_stock_kline_with_indicators( + self, + market: int, + code: str, + indicators: list[str], + period: Period = Period.DAILY, + count: int = 30, + adjust: Adjust = Adjust.QFQ, + params: dict[str, dict[str, int | float]] | None = None, + ) -> pd.DataFrame: + """获取 K 线数据并计算技术指标。 + + 自动获取足够的历史数据用于指标预热(EMA 至少需要 120 周期)。 + + Args: + market: 市场代码。 + code: 股票代码。 + indicators: 指标名称列表,如 ``["MACD", "KDJ"]``。 + period: K 线周期。 + count: 返回条数(默认30)。 + adjust: 复权方式(默认前复权)。 + params: 可选指标参数覆盖。 + """ + from ..indicator import compute_indicators + + fetch_count = max(120 + count, 200) + df = self.get_stock_kline(market, code, period=period, count=fetch_count, adjust=adjust) + if df.empty: + return df + return compute_indicators(df, indicators, params, tail=count) + # ------------------------------------------------------------------ # # 分时 # ------------------------------------------------------------------ # @@ -1140,6 +1171,30 @@ class AsyncMacClient: return _to_df(all_bars) + async def get_stock_kline_with_indicators( + self, + market: int, + code: str, + indicators: list[str], + period: Period = Period.DAILY, + count: int = 30, + adjust: Adjust = Adjust.QFQ, + params: dict[str, dict[str, int | float]] | None = None, + ) -> pd.DataFrame: + """获取 K 线数据并计算技术指标(异步)。 + + 自动获取足够的历史数据用于指标预热(EMA 至少需要 120 周期)。 + """ + from ..indicator import compute_indicators + + fetch_count = max(120 + count, 200) + df = await self.get_stock_kline( + market, code, period=period, count=fetch_count, adjust=adjust, + ) + if df.empty: + return df + return compute_indicators(df, indicators, params, tail=count) + # ------------------------------------------------------------------ # # 分时 # ------------------------------------------------------------------ # diff --git a/src/easy_tdx/unified.py b/src/easy_tdx/unified.py index af18a97..7bde9e2 100644 --- a/src/easy_tdx/unified.py +++ b/src/easy_tdx/unified.py @@ -125,6 +125,20 @@ class UnifiedTdxClient: ) -> pd.DataFrame: return self._ensure_mac().get_stock_kline(market, code, period, start, count, times, adjust) + def get_stock_kline_with_indicators( + self, + market: int, + code: str, + indicators: list[str], + period: Period = Period.DAILY, + count: int = 30, + adjust: Adjust = Adjust.QFQ, + params: dict[str, dict[str, int | float]] | None = None, + ) -> pd.DataFrame: + return self._ensure_mac().get_stock_kline_with_indicators( + market, code, indicators, period, count, adjust, params, + ) + def get_tick_chart( self, market: int, @@ -397,6 +411,21 @@ class AsyncUnifiedTdxClient: mac = await self._ensure_mac() return await mac.get_stock_kline(market, code, period, start, count, times, adjust) + async def get_stock_kline_with_indicators( + self, + market: int, + code: str, + indicators: list[str], + period: Period = Period.DAILY, + count: int = 30, + adjust: Adjust = Adjust.QFQ, + params: dict[str, dict[str, int | float]] | None = None, + ) -> pd.DataFrame: + mac = await self._ensure_mac() + return await mac.get_stock_kline_with_indicators( + market, code, indicators, period, count, adjust, params, + ) + async def get_tick_chart( self, market: int, diff --git a/tests/unit/test_indicator.py b/tests/unit/test_indicator.py new file mode 100644 index 0000000..f4757ec --- /dev/null +++ b/tests/unit/test_indicator.py @@ -0,0 +1,158 @@ +"""indicator.py 离线单元测试。""" + +from __future__ import annotations + +import warnings + +import numpy as np +import pandas as pd +import pytest + +from easy_tdx.indicator import compute_indicators, list_indicators, _REGISTRY + + +def _make_ohlcv(n: int = 200, seed: int = 42) -> pd.DataFrame: + rng = np.random.default_rng(seed) + close = 100 + np.cumsum(rng.standard_normal(n) * 0.5) + high = close + np.abs(rng.standard_normal(n)) + low = close - np.abs(rng.standard_normal(n)) + open_ = low + (high - low) * rng.random(n) + vol = (rng.random(n) * 1e6).astype(float) + return pd.DataFrame({ + "datetime": pd.date_range("2024-01-01", periods=n, freq="D"), + "open": open_, + "high": high, + "low": low, + "close": close, + "vol": vol, + "amount": vol * close, + }) + + +class TestRegistry: + def test_all_indicators_registered(self): + assert len(_REGISTRY) >= 22 + + def test_list_indicators_returns_metadata(self): + info = list_indicators() + assert len(info) >= 22 + for entry in info: + assert "name" in entry + assert "inputs" in entry + assert "outputs" in entry + assert "description" in entry + + +class TestComputeIndicators: + def test_single_indicator_macd(self): + df = _make_ohlcv() + result = compute_indicators(df, ["MACD"]) + assert "MACD_DIF" in result.columns + assert "MACD_DEA" in result.columns + assert "MACD_HIST" in result.columns + assert len(result) == 200 + + def test_multiple_indicators(self): + df = _make_ohlcv() + result = compute_indicators(df, ["MACD", "KDJ", "RSI"]) + for col in ["MACD_DIF", "MACD_DEA", "MACD_HIST", "KDJ_K", "KDJ_D", "KDJ_J", "RSI"]: + assert col in result.columns + + def test_keep_ohlcv_true(self): + df = _make_ohlcv() + result = compute_indicators(df, ["RSI"], keep_ohlcv=True) + for col in ["open", "high", "low", "close", "vol"]: + assert col in result.columns + + def test_keep_ohlcv_false(self): + df = _make_ohlcv() + result = compute_indicators(df, ["RSI"], keep_ohlcv=False) + assert "close" not in result.columns + assert "RSI" in result.columns + # datetime 应保留 + assert "datetime" in result.columns + + def test_keep_ohlcv_false_no_time_cols(self): + df = _make_ohlcv() + df = df.drop(columns=["datetime"]) + result = compute_indicators(df, ["RSI"], keep_ohlcv=False) + assert "close" not in result.columns + assert "RSI" in result.columns + + def test_tail_parameter(self): + df = _make_ohlcv(200) + result = compute_indicators(df, ["MACD"], tail=30) + assert len(result) == 30 + assert "MACD_DIF" in result.columns + + def test_case_insensitive(self): + df = _make_ohlcv() + result = compute_indicators(df, ["macd", "kdj"]) + assert "MACD_DIF" in result.columns + assert "KDJ_K" in result.columns + + def test_custom_params(self): + df = _make_ohlcv() + r1 = compute_indicators(df, ["MACD"]) + r2 = compute_indicators(df, ["MACD"], params={"MACD": {"SHORT": 10}}) + # 不同参数应产生不同结果 + assert not np.allclose(r1["MACD_DIF"].values, r2["MACD_DIF"].values, equal_nan=True) + + def test_unknown_indicator_raises(self): + df = _make_ohlcv() + with pytest.raises(ValueError, match="未知指标"): + compute_indicators(df, ["FAKE_INDICATOR"]) + + def test_missing_input_columns_raises(self): + df = pd.DataFrame({"close": np.random.randn(200)}) + with pytest.raises(ValueError, match="缺少必要列"): + compute_indicators(df, ["KDJ"]) + + def test_empty_dataframe(self): + df = pd.DataFrame() + result = compute_indicators(df, ["MACD"]) + assert result.empty + + def test_short_data_warning(self): + df = _make_ohlcv(50) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + compute_indicators(df, ["MACD"]) + assert any("120" in str(warning.message) for warning in w) + + def test_rsi_range(self): + df = _make_ohlcv(200) + result = compute_indicators(df, ["RSI"]) + rsi = result["RSI"].dropna() + assert (rsi >= -10).all() and (rsi <= 110).all() + + def test_boll_bands_order(self): + df = _make_ohlcv(200) + result = compute_indicators(df, ["BOLL"]) + valid = result.dropna(subset=["BOLL_UPPER", "BOLL_LOWER"]) + assert (valid["BOLL_UPPER"] >= valid["BOLL_LOWER"]).all() + + def test_obv_with_volume(self): + df = _make_ohlcv() + result = compute_indicators(df, ["OBV"]) + assert "OBV" in result.columns + + def test_brar_needs_open(self): + df = _make_ohlcv() + result = compute_indicators(df, ["BRAR"]) + assert "AR" in result.columns + assert "BR" in result.columns + + def test_all_registered_indicators_run(self): + """确保所有注册的指标都能无错运行。""" + df = _make_ohlcv(250) + for name in _REGISTRY: + result = compute_indicators(df, [name]) + spec = _REGISTRY[name] + for col in spec.outputs: + assert col in result.columns, f"{name} missing output {col}" + + def test_result_index_reset(self): + df = _make_ohlcv() + result = compute_indicators(df, ["RSI"]) + assert list(result.index) == list(range(len(result)))