From a12c2122fc31a1b24ffe961e9b44039976b36cb9 Mon Sep 17 00:00:00 2001 From: Justin Gu <97915@qq.com> Date: Sun, 14 Jun 2026 18:46:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(indicator):=20=E6=96=B0=E5=A2=9E=20SAR/VWA?= =?UTF-8?q?P/AROON=20=E4=B8=89=E6=8C=87=E6=A0=87=20+=20=E6=B3=A8=E5=86=8C?= =?UTF-8?q?=20FK=EF=BC=8830=20=E2=86=92=2034=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 37 +++++-- pyproject.toml | 2 +- src/easy_tdx/MyTT.py | 63 +++++++++++ src/easy_tdx/MyTT.pyi | 14 +++ src/easy_tdx/indicator.py | 40 +++++++ tests/unit/test_mytt.py | 216 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 365 insertions(+), 7 deletions(-) create mode 100644 tests/unit/test_mytt.py diff --git a/README.md b/README.md index 25d417c..62ffe2e 100644 --- a/README.md +++ b/README.md @@ -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 | VWAP(N日滚动成交量加权均价) | +| AROON | high, low | AROON_UP, AROON_DOWN, AROON_OSC | +| FK | close | FK(EMA(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 / AsyncMacClient(MAC 协议) @@ -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 根为 nan(rolling 窗口) +- 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 strict(13 errors)和 ruff format(8 files)失败,全部为类型标注与存根问题,无运行时行为变更。 diff --git a/pyproject.toml b/pyproject.toml index 3395693..b57f17c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/src/easy_tdx/MyTT.py b/src/easy_tdx/MyTT.py index 1103835..dc336e4 100644 --- a/src/easy_tdx/MyTT.py +++ b/src/easy_tdx/MyTT.py @@ -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 diff --git a/src/easy_tdx/MyTT.pyi b/src/easy_tdx/MyTT.pyi index 4f85dad..fa2d79f 100644 --- a/src/easy_tdx/MyTT.pyi +++ b/src/easy_tdx/MyTT.pyi @@ -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 ──────────────────────────────────────────────────────── diff --git a/src/easy_tdx/indicator.py b/src/easy_tdx/indicator.py index bdffd93..2b97cea 100644 --- a/src/easy_tdx/indicator.py +++ b/src/easy_tdx/indicator.py @@ -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]]: """返回所有可用指标的元数据。""" diff --git a/tests/unit/test_mytt.py b/tests/unit/test_mytt.py new file mode 100644 index 0000000..901e133 --- /dev/null +++ b/tests/unit/test_mytt.py @@ -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 根应为 nan(rolling 窗口未填满) + _, 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