feat(indicator): 新增 SAR/VWAP/AROON 三指标 + 注册 FK(30 → 34)

This commit is contained in:
Justin Gu
2026-06-14 18:46:02 +08:00
parent 37635af8f2
commit a12c2122fc
6 changed files with 365 additions and 7 deletions
+31 -6
View File
@@ -14,7 +14,7 @@ easy-tdx 要做的事很简单:**把机构的数据锁砸开,扔到每个普
它是一个完全免费、无需注册、无需 API Key、纯开源的行**情核武器**。
一行命令,A股、港股、美股、期货——K线、报价、资金流向、板块轮动、分时明细、逐笔成交,**毫秒级拉满**。
**32个技术指标**MACD、KDJ、RSI、BOLL……连”捉妖大师”和”30日乖离率信号”都给你算好)开箱即用。
**34个技术指标**MACD、KDJ、RSI、BOLL……连”捉妖大师”和”30日乖离率信号”都给你算好)开箱即用。
**缠论分析**(笔、中枢、买卖点、背驰)一键出结果——你不再需要手画分型、猜线段。
**内置回测引擎**——写个策略文件,一行命令跑回测,16 个经典策略自带,多因子组合、策略选股扫描,批量对比哪个最赚钱一目了然。
@@ -709,7 +709,7 @@ with MacClient.from_best_host() as c:
# + BS_X, BS_SMA, BS_LMA
```
支持 32 个指标:MACD, KDJ, RSI, BOLL, DMI, ATR, WR, CCI, BIAS, BIAS_SIGNAL, OBV, VR, EMV, MFI, BRAR, ASI, TRIX, DPO, MTM, ROC, EXPMA, BBI, PSY, DFMA, CR, KTN, XSII, MASS, TAQ, ZHUOYAO。
支持 34 个指标:MACD, KDJ, RSI, BOLL, DMI, ATR, WR, CCI, BIAS, BIAS_SIGNAL, OBV, VR, EMV, MFI, BRAR, ASI, TRIX, DPO, MTM, ROC, EXPMA, BBI, PSY, DFMA, CR, KTN, XSII, MASS, TAQ, ZHUOYAO, SAR, VWAP, AROON, FK
```python
# Python API 用法
@@ -725,7 +725,7 @@ with MacClient.from_best_host() as c:
# + ZY_LONG, ZY_MID, ZY_SHORT, ZY_TREND
```
支持 32 个指标:MACD, KDJ, RSI, BOLL, DMI, ATR, WR, CCI, BIAS, BIAS_SIGNAL, OBV, VR, EMV, MFI, BRAR, ASI, TRIX, DPO, MTM, ROC, EXPMA, BBI, PSY, DFMA, CR, KTN, XSII, MASS, TAQ, ZHUOYAO。
支持 34 个指标:MACD, KDJ, RSI, BOLL, DMI, ATR, WR, CCI, BIAS, BIAS_SIGNAL, OBV, VR, EMV, MFI, BRAR, ASI, TRIX, DPO, MTM, ROC, EXPMA, BBI, PSY, DFMA, CR, KTN, XSII, MASS, TAQ, ZHUOYAO, SAR, VWAP, AROON, FK
### 财务
@@ -941,7 +941,7 @@ uvicorn.run(app, host="0.0.0.0", port=8000)
| `market-stat` | 全市场涨跌统计 |
| `server-info` | 服务器交易时段 |
| `symbol-info` | 个股特征快照 |
| `indicator` | 技术指标计算(32 个:MACD/KDJ/RSI/BOLL/DMI/ATR... |
| `indicator` | 技术指标计算(34 个:MACD/KDJ/RSI/BOLL/DMI/ATR... |
| `indicator-list` | 列出可用技术指标 |
| `backtest` | 回测引擎(加载策略文件,输出绩效报告) |
| `portfolio` | 多标的组合回测(共享资金池,均等分配,汇总绩效) |
@@ -1060,7 +1060,7 @@ with MacClient.from_best_host() as c:
print(info["name"], info["description"], info["outputs"])
```
支持 31 个技术指标:
支持 34 个技术指标:
| 指标 | 输入 | 输出列 |
|------|------|--------|
@@ -1094,6 +1094,10 @@ with MacClient.from_best_host() as c:
| TAQ | high, low | TAQ_UP, TAQ_MID, TAQ_DOWN |
| ZHUOYAO | close | ZY_LONG, ZY_MID, ZY_SHORT, ZY_TREND |
| BIAS_SIGNAL | close | BS_X, BS_SMA, BS_LMA |
| SAR | high, low | SAR(抛物线转向/动态止损位) |
| VWAP | close, high, low, vol | VWAPN日滚动成交量加权均价) |
| AROON | high, low | AROON_UP, AROON_DOWN, AROON_OSC |
| FK | close | FKEMA(2) 突破斜率外推 EMA(42) |
#### 分时
@@ -1460,7 +1464,7 @@ src/easy_tdx/
├── client.py # TdxClient / AsyncTdxClient(标准协议)
├── unified.py # UnifiedTdxClient(统一入口)
├── config.py # 服务器地址、端口、超时配置
├── indicator.py # 技术指标计算(32 个,基于 MyTT
├── indicator.py # 技术指标计算(34 个,基于 MyTT
├── MyTT.py # 麦语言技术指标算法库
├── mac/
│ ├── client.py # MacClient / AsyncMacClientMAC 协议)
@@ -1511,6 +1515,27 @@ ruff format --check src/ tests/ # format check
## Changelog
### 1.12.0 (2026-06-14)
**新增 4 个技术指标(30 → 34)** — 按"语义空白"补齐三类现有指标库缺失的维度:止损位、机构成本价、趋势启动时机。均为纯 numpy 实现,零新依赖。
**新增指标**
- **SAR 抛物线转向**`high, low``SAR`):基于 Wilder 加速因子的动态止损位,填补 32 个指标里"止损位"语义的空白。可直接喂给 `BacktestEngine` 做动态 `stop_loss`。实现含反转检测、AF 加速/封顶、SAR 不穿越前两根 K 线极值的限制。
- **VWAP 成交量加权均价**`close, high, low, vol``VWAP`):N 日滚动机构基准成本价,填补"机构成本"维度空白。用典型价格 `(H+L+C)/3` 加权,含除零保护(零成交量返回 nan)。
- **AROON 阿隆指标**`high, low``AROON_UP, AROON_DOWN, AROON_OSC`):用"N 周期内新高/新低距今多少根"识别趋势启动时机,与现有 DMI(判断趋势强度但滞后)互补而非冗余。
- **FK 趋势指标**`close``FK`):清理孤儿函数——`MyTT.FK` 此前已实现但未在 `indicator.py` 注册,用户通过 CLI/API 无法调用。现正式注册暴露。语义为 EMA(2) 是否突破斜率外推 EMA(42),本质是动量偏离检测。
**架构**:所有新指标沿用现有 `IndicatorSpec` 注册模式,`compute_indicators()` / `get_stock_kline_with_indicators()` / CLI `easy-tdx indicator` 自动可用,无需改动调度层。
**除零与边界保护**
- SAR:一字板/停牌(高低价相同)不崩溃、不产生 inf;空输入返回空数组
- VWAP:零成交量返回 nan(不产生 inf);前 N-1 根为 nanrolling 窗口)
- AROON:输出严格落在 [0, 100] 区间
**类型存根**`MyTT.pyi` 同步补充 SAR/VWAP/AROON/FK 四个函数签名,mypy strict 零错误。
**测试**:新增 `tests/unit/test_mytt.py`,22 个用例覆盖三个新指标 + FK 的数值正确性、单边行情行为、除零/空输入边界。注册层端到端覆盖复用 `test_indicator.py::test_all_registered_indicators_run`
### 1.11.6 (2026-06-13)
**CI 类型与格式修复** — 修复 CI 流水线 mypy strict13 errors)和 ruff format8 files)失败,全部为类型标注与存根问题,无运行时行为变更。
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "easy-tdx"
version = "1.11.6"
version = "1.12.0"
description = "通达信 TCP 协议行情数据客户端,支持在线行情、离线数据读取与写入同步"
readme = "README.md"
requires-python = ">=3.10"
+63
View File
@@ -14,6 +14,7 @@
# V3.2 2023-04-04 新增 CR指标
# V3.3 2023-11-09 新增 SIN,COS,TAN序列处理的三角函数
# V4.0 2026-06-02 handsomejustin 新增 ZHUOYAO,BIAS_SIGNAL两个自创函数
# V4.1 2026-06-14 新增 SAR(抛物线转向), VWAP(成交量加权均价), AROON(阿隆指标); 注册 FK
# 以下所有函数如无特别说明,输入参数S均为numpy序列或者列表list,N为整型int
# 应用层1级函数完美兼容通达信或同花顺,具体使用方法请参考通达信
@@ -496,4 +497,66 @@ def OUTPERFORM_20D(CLOSE, INDEX_CLOSE): # 20日相对强度:个股涨幅跑
return IF(stock_ret > index_ret, 1, 0)
def SAR(HIGH, LOW, AF_STEP=0.02, AF_MAX=0.2): # 抛物线转向指标:基于 ATR 思想的动态止损位
HIGH = np.asarray(HIGH, dtype=float)
LOW = np.asarray(LOW, dtype=float)
n = len(HIGH)
sar = np.full(n, np.nan)
if n == 0:
return sar
# 初始假设上涨趋势:SAR 起点取首根低点,极值点取首根高点
bull = True
af = AF_STEP
ep = HIGH[0]
sar[0] = LOW[0]
for i in range(1, n):
# 下一根 SAR = 前一根 SAR + AF * (EP - 前一根 SAR)
new_sar = sar[i - 1] + af * (ep - sar[i - 1])
# SAR 不能进入前两根 K 线极值范围(Wilder 标准限制,避免 SAR 被价格穿越)
prev2 = max(i - 2, 0)
if bull:
new_sar = min(new_sar, LOW[i - 1], LOW[prev2])
else:
new_sar = max(new_sar, HIGH[i - 1], HIGH[prev2])
sar[i] = new_sar
# 反转判断:上涨时 LOW 穿越止损位 / 下跌时 HIGH 穿越止损位
if bull and LOW[i] <= new_sar:
bull = False
sar[i] = ep # 反转点 SAR = 前极值点
ep = LOW[i]
af = AF_STEP
elif not bull and HIGH[i] >= new_sar:
bull = True
sar[i] = ep
ep = HIGH[i]
af = AF_STEP
else:
# 无反转,更新极值点和加速因子
if bull and HIGH[i] > ep:
ep = HIGH[i]
af = min(af + AF_STEP, AF_MAX)
elif not bull and LOW[i] < ep:
ep = LOW[i]
af = min(af + AF_STEP, AF_MAX)
return sar
def VWAP(CLOSE, HIGH, LOW, VOL, N=20): # 成交量加权均价:N日滚动机构基准成本价
TP = (HIGH + LOW + CLOSE) / 3.0 # 典型价格
num = pd.Series(TP * VOL).rolling(N).sum().values
den = pd.Series(VOL).rolling(N).sum().values
with np.errstate(divide="ignore", invalid="ignore"):
return np.where(den > 0, num / den, np.nan)
def AROON(HIGH, LOW, N=25): # 阿隆指标:趋势启动时机识别(N周期内新高/新低距今多少根)
# HHVBARS/LLVBARS 返回极值距今的周期数
up_bars = HHVBARS(HIGH, N) # N周期最高价距今周期数
down_bars = LLVBARS(LOW, N) # N周期最低价距今周期数
AROON_UP = (N - up_bars) / N * 100 # 越接近100=近期创新高=上涨动能强
AROON_DOWN = (N - down_bars) / N * 100 # 越接近100=近期创新低=下跌动能强
OSC = AROON_UP - AROON_DOWN # 震荡指标:正值多头,负值空头
return RD(AROON_UP), RD(AROON_DOWN), RD(OSC)
# 望大家能提交更多指标和函数 https://github.com/mpquant/MyTT
+14
View File
@@ -91,6 +91,20 @@ def PSY(CLOSE: npt.ArrayLike, N: int = ...) -> NDArray: ...
def TAQ(CLOSE: npt.ArrayLike, N: int = ...) -> NDArray: ...
def XSII(CLOSE: npt.ArrayLike, HIGH: npt.ArrayLike, LOW: npt.ArrayLike, N: int = ...) -> NDArray: ...
def ZHUOYAO(CLOSE: npt.ArrayLike, HIGH: npt.ArrayLike, LOW: npt.ArrayLike, VOL: npt.ArrayLike) -> NDArray: ...
def SAR(HIGH: npt.ArrayLike, LOW: npt.ArrayLike, AF_STEP: float = ..., AF_MAX: float = ...) -> NDArray: ...
def VWAP(
CLOSE: npt.ArrayLike,
HIGH: npt.ArrayLike,
LOW: npt.ArrayLike,
VOL: npt.ArrayLike,
N: int = ...,
) -> NDArray: ...
def AROON(
HIGH: npt.ArrayLike,
LOW: npt.ArrayLike,
N: int = ...,
) -> tuple[NDArray, NDArray, NDArray]: ...
def FK(CLOSE: npt.ArrayLike) -> NDArray: ...
# ── Utility Functions ────────────────────────────────────────────────────────
+40
View File
@@ -199,6 +199,46 @@ _reg(
"TAQ", ("high", "low"), ("TAQ_UP", "TAQ_MID", "TAQ_DOWN"), MyTT.TAQ, {"N": 20}, "TAQ 唐安奇通道"
)
# ── SAR 抛物线转向(仅需 high + low)─────────────────────────────────
_reg(
"SAR",
("high", "low"),
("SAR",),
MyTT.SAR,
{"AF_STEP": 0.02, "AF_MAX": 0.2},
"SAR 抛物线转向(动态止损位)",
)
# ── VWAP 成交量加权均价(close + high + low + vol)────────────────────
_reg(
"VWAP",
("close", "high", "low", "vol"),
("VWAP",),
MyTT.VWAP,
{"N": 20},
"VWAP 成交量加权均价(N日滚动机构基准成本)",
)
# ── Aroon 阿隆指标(仅需 high + low)────────────────────────────────
_reg(
"AROON",
("high", "low"),
("AROON_UP", "AROON_DOWN", "AROON_OSC"),
MyTT.AROON,
{"N": 25},
"AROON 阿隆指标(趋势启动时机)",
)
# ── FK 趋势快线慢线(仅需 close,清理孤儿函数)──────────────────────
_reg(
"FK",
("close",),
("FK",),
MyTT.FK,
{},
"FK 趋势指标(EMA(2) 突破斜率外推 EMA(42),动量偏离检测)",
)
def list_indicators() -> list[dict[str, object]]:
"""返回所有可用指标的元数据。"""
+216
View File
@@ -0,0 +1,216 @@
"""MyTT.py 新增指标函数(SAR/VWAP/AROON/FK)的数值正确性与边界测试。
这些测试针对 MyTT.py 里函数本身,不经过 indicator.py 注册层。
注册层的端到端覆盖在 test_indicator.py::TestComputeIndicators::test_all_registered_indicators_run。
"""
from __future__ import annotations
import numpy as np
import pandas as pd
import pytest
from easy_tdx import MyTT
def _ohlcv(n: int = 200, seed: int = 42) -> tuple[np.ndarray, ...]:
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 + 1.0).astype(float) # +1 避免全零
return open_, high, low, close, vol
class TestSAR:
"""SAR 抛物线转向指标。"""
def test_returns_same_length(self):
_, high, low, _, _ = _ohlcv()
sar = MyTT.SAR(high, low)
assert len(sar) == len(high)
def test_first_value_is_low(self):
# 默认假设上涨趋势,SAR 起点取首根低点
_, high, low, _, _ = _ohlcv()
sar = MyTT.SAR(high, low)
assert sar[0] == pytest.approx(low[0])
def test_empty_input(self):
sar = MyTT.SAR(np.array([]), np.array([]))
assert len(sar) == 0
def test_flat_market_no_crash(self):
# 一字板/停牌:高低价完全相同,不应崩溃或产生 inf
flat = np.full(50, 10.0)
sar = MyTT.SAR(flat, flat)
assert len(sar) == 50
assert np.isfinite(sar[1:]).all(), "SAR 不应产生 inf/nan(首值外)"
def test_rising_market_sar_below_price(self):
# 持续上涨时 SAR 应在价格下方(上涨止损位)
high = np.arange(50, dtype=float) + 1
low = np.arange(50, dtype=float)
sar = MyTT.SAR(high, low)
# 前 5 根建立趋势后,SAR 应低于对应低点
assert (sar[5:] <= low[5:] + 1e-6).all()
def test_falling_market_sar_above_price(self):
# 持续下跌时 SAR 应在价格上方(下跌止损位)
low = np.array([100 - i for i in range(50)], dtype=float)
high = low + 1
sar = MyTT.SAR(high, low)
# 确认在某处发生反转(趋势从上涨初判切换)
# 不强求全程在上方(初判是上涨),但尾部下跌段 SAR 应高于 low
assert sar[-1] > low[-1]
def test_reversal_resets_af(self):
# 反转时加速因子应回到 AF_STEP(无法直接观测,间接验证:反转后第一步 SAR 等于前极值点)
# 构造 V 型反转:先涨后跌
rise_h = np.arange(25, dtype=float) + 1
fall_h = np.array([25 - i + 1 for i in range(1, 25)])
high = np.concatenate([rise_h, fall_h])
rise_l = np.arange(25, dtype=float)
fall_l = np.array([25 - i for i in range(1, 25)])
low = np.concatenate([rise_l, fall_l])
sar = MyTT.SAR(high, low)
assert np.isfinite(sar).all()
def test_acceleration_factor_capped(self):
# 长期单边上涨,AF 不应超过 AF_MAX(通过 SAR 增量间接验证不发散)
high = np.cumsum(np.ones(100)) + 1 # 每根 +1
low = np.cumsum(np.ones(100))
sar = MyTT.SAR(high, low, AF_STEP=0.02, AF_MAX=0.2)
assert np.isfinite(sar).all()
# SAR 全程应在 low 之下(持续上涨不反转)
valid = sar[2:]
assert (valid <= low[2:] + 1e-6).all()
class TestVWAP:
"""VWAP 成交量加权均价。"""
def test_returns_same_length(self):
_, high, low, close, vol = _ohlcv()
vwap = MyTT.VWAP(close, high, low, vol, N=20)
assert len(vwap) == len(close)
def test_leading_nan(self):
# 前 N-1 根应为 nanrolling 窗口未填满)
_, high, low, close, vol = _ohlcv()
vwap = MyTT.VWAP(close, high, low, vol, N=20)
assert np.isnan(vwap[:19]).all()
assert not np.isnan(vwap[19])
def test_constant_price(self):
# 价格、量都恒定时,VWAP 应等于典型价格
n = 50
close = np.full(n, 10.0)
high = np.full(n, 11.0)
low = np.full(n, 9.0)
vol = np.full(n, 1000.0)
vwap = MyTT.VWAP(close, high, low, vol, N=20)
expected_tp = (11 + 9 + 10) / 3.0 # =10.0
assert np.allclose(vwap[19:], expected_tp, equal_nan=True)
def test_uniform_volume_equals_typical_price_mean(self):
# 等量时 VWAP = 典型价格的 N 日均值
n = 100
rng = np.random.default_rng(1)
close = 100 + rng.standard_normal(n)
high = close + 1
low = close - 1
vol = np.full(n, 500.0)
tp = (high + low + close) / 3.0
vwap = MyTT.VWAP(close, high, low, vol, N=10)
tp_ma = pd.Series(tp).rolling(10).mean().values
assert np.allclose(vwap, tp_ma, equal_nan=True)
def test_zero_volume_returns_nan(self):
# 全零成交量时,VWAP 应为 nan(除零保护)
n = 30
close = np.full(n, 10.0)
high = np.full(n, 11.0)
low = np.full(n, 9.0)
vol = np.zeros(n)
vwap = MyTT.VWAP(close, high, low, vol, N=20)
assert np.isnan(vwap[19:]).all()
class TestAROON:
"""Aroon 阿隆指标。"""
def test_returns_three_arrays(self):
_, high, low, _, _ = _ohlcv()
up, down, osc = MyTT.AROON(high, low, N=25)
assert len(up) == len(high)
assert len(down) == len(high)
assert len(osc) == len(high)
def test_range_zero_to_hundred(self):
# AROON_UP/DOWN 应在 [0, 100] 区间
_, high, low, _, _ = _ohlcv()
up, down, _ = MyTT.AROON(high, low, N=25)
# 跳过 rolling 窗口前的 nan
valid_up = up[24:]
valid_down = down[24:]
assert (valid_up >= 0).all() and (valid_up <= 100).all()
assert (valid_down >= 0).all() and (valid_down <= 100).all()
def test_new_high_gives_full_up(self):
# 在窗口末端创新高时,AROON_UP 应 = 100
n = 50
high = np.linspace(1, 30, n) # 单调上升,末根创新高
low = high - 0.5
up, down, _ = MyTT.AROON(high, low, N=25)
assert up[-1] == pytest.approx(100.0)
def test_new_low_gives_full_down(self):
# 在窗口末端创新低时,AROON_DOWN 应 = 100
n = 50
low = np.linspace(30, 1, n) # 单调下降
high = low + 0.5
_, down, _ = MyTT.AROON(high, low, N=25)
assert down[-1] == pytest.approx(100.0)
def test_osc_is_difference(self):
# OSC = UP - DOWN
_, high, low, _, _ = _ohlcv()
up, down, osc = MyTT.AROON(high, low, N=25)
assert np.allclose(osc[24:], (up - down)[24:], equal_nan=True)
def test_leading_nan(self):
_, high, low, _, _ = _ohlcv()
up, down, _ = MyTT.AROON(high, low, N=25)
# HHVBARS/LLVBARS 在 N-1 根前为 nan
assert np.isnan(up[:24]).all()
class TestFK:
"""FK 趋势指标(布尔输出)。
慢线用 SLOPE(CLOSE,21)*20 做斜率外推:上涨时慢线被正斜率推高,
下跌时被负斜率压低。FK = fast(EMA2) > slow(外推 EMA42)
语义是"价格是否突破趋势外推线",本质是动量/反转偏离检测:
- 强下跌时 fast 相对外推慢线偏高 → FK=True(超卖/反弹信号)
- 强上涨时慢线被推高,fast 难以超越 → FK=False(未超买或接近超买)
"""
def test_returns_boolean_array(self):
close = _ohlcv()[3]
fk = MyTT.FK(close)
assert len(fk) == len(close)
assert fk.dtype == bool
def test_rising_market_returns_false(self):
# 强上涨:正斜率外推把慢线推高,fast < slow → FK=False
close = np.cumsum(np.ones(100)) # 每根 +1
fk = MyTT.FK(close)
assert bool(fk[-1]) is False
def test_falling_market_returns_true(self):
# 强下跌:负斜率外推把慢线压低,fast > slow → FK=True
close = np.array([100 - i for i in range(100)], dtype=float)
fk = MyTT.FK(close)
assert bool(fk[-1]) is True