release: v1.30.2 — 无未来函数指标扩容16个(注册指标50个)+ 内置策略补齐54个:WebUI回测全覆盖,新增前缀一致性无未来函数回归测试

This commit is contained in:
Justin Gu
2026-09-03 03:07:57 +08:00
parent 1433f4eed0
commit 62e80d892e
11 changed files with 2496 additions and 7 deletions
+23
View File
@@ -2,6 +2,29 @@
本文件记录 easy-tdx 的版本变更。格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/)。
## [1.30.2] — 2026-09-03
**无未来函数指标扩容 16 个 + WebUI 回测策略补齐至 54 个**——本轮从互联网主流指标库(TradingView 热榜 / StockCharts / 通达信社区)甄别出一批**计算上只引用当期及历史数据、信号不漂移**的经典指标补进 MyTT,并把回测端"有指标无策略"的缺口全部填平:此前 WebUI 回测下拉只有 19 个内置策略,34 个注册指标里近半数"看得见指标、跑不了回测"——现在**50 个注册指标、54 个内置策略一一对应**,回测页下拉、CLI、Web API 三端自动同步。
### 新增(指标,MyTT V4.316 个函数)
- **趋势/止损类**`SUPERTREND` 超级趋势(Wilder ATR 递推锁带,带线即移动止损,2025 年 TradingView 各"最佳指标"榜单常客)、`CHANDELIER` 吊灯止损(Chuck LeBeauHHVK×ATR 经典离场位)、`HMA` 赫尔均线(低滞后不重绘)、`KAMA` 考夫曼自适应均线(效率比驱动:趋势市贴价、震荡市自动走平)、`ICHIMOKU` 一目均衡表(五线一云;先行带用 26 期前的值画到当前位置,只引用过去数据——**迟行带 CHIKOU 按定义引用未来收盘,仅作图示对齐,当期信号严禁使用**,已在 docstring 与注册描述中显著标注)。
- **动量/振荡类**`UOS` 终极指标(Larry Williams7/14/28 三周期加权)、`CMO` 钱德动量振荡器、`TSI` 真实强度指数(双重 EMA 平滑,深回调钝化少)、`FISHER` 费雪变换(John Ehlers,拐点尖锐无钝化)。
- **波动率/状态识别类**`SQUEEZE` TTM 挤压动量(布林带收进肯特纳通道=挤压态 + 线性回归动量定方向)、`CHOP` 盘整指数(>61.8 震荡 / <38.2 趋势,策略侧稀缺的"行情状态开关")、`BBP` 布林 %B 位置、`BBW` 布林带宽(收窄=变盘预警)。
- **量能/资金类**`AD` 累积/派发线(Marc Chaikin)、`CMF` 佳庆资金流量、`EFI` 艾尔德强力指数——补齐此前最薄弱的"资金流向"一块。
- 明确排除项维持 v1.30.1 的口径:ZigZag 家族、`FINDHIGH/FINDLOW/BACKSET` 等通达信未来函数、需右侧 K 线确认的 Bill Williams Fractals、需要 tick 级数据的 Volume Profile 均不引入。
### 新增(策略,35 个,内置策略 19 → 54)
- **存量指标补策略(19 个)**`psy_reversal` / `mtm_cross` / `roc_zero` / `expma_cross` / `dfma_cross` / `cr_reversal` / `xsii_breakout` / `obv_cross` / `vr_reversal` / `mass_cross` / `mfi_reversal` / `brar_reversal` / `asi_cross` / `zhuoyao_trend`(多周期涨幅共振)/ `bias_signal_cross` / `sar_follow` / `vwap_cross` / `aroon_cross` / `fk_reversal`
- **新指标首发策略(16 个)**`supertrend`(方向翻转跟随)/ `kama_cross` / `hma_cross` / `chandelier`(通道突破进场+吊灯止损离场)/ `ichimoku_cross`(转换/基准线金叉+云层位置确认)/ `uos_reversal` / `cmo_reversal` / `tsi_cross` / `fisher_cross` / `squeeze_breakout`(挤压解除+动量方向)/ `chop_trend`(趋态开关+均线方向)/ `ad_cross` / `cmf_zero` / `efi_zero` / `bbp_reversal` / `bbw_squeeze`。全部实现 `entry_exit_masks`,走上 v1.28 的向量化快速路径。
### 测试与防回归
- **无未来函数前缀一致性回归(`TestNoLookahead`)**:把 200 根 K 线截断到前 120 根,16 个新指标的输出在重叠段必须与"仅用前缀数据计算"逐位一致——未来函数的致命特征是后到数据改写历史输出,任何引用 t+1 之后数据的实现当场爆红。该测试与黄金基线、向量化对拍构成三重防线。
- 向量化对拍扩至 74 例全绿(54 策略逐 bar vs 向量化逐位一致且全部实际产生交易);黄金基线 `backtest_metrics.json` 重生成至 54 策略全量锁定,**19 个存量策略数字一位未动**(存量行为零漂移)。`test_mytt.py` 新增 47 例数值正确性测试(范围/预热/公式口径/一字板除零保护)。
- 文档同步:README 策略计数 19 → 54,《回测系统完全上手手册》陈旧的"18 个内置策略"修正。
## [1.30.1] — 2026-09-03
**彻底移除 ZIG 之字转向指标与 `zig_breakout` 策略**——ZIG 是教科书级的**未来函数**:波峰/波谷拐点只有在**其后**的走势反向走满 X% 确认转向后才会**回溯标出**,也就是说序列里每个拐点的位置都用到了"当时不可能知道"的未来信息。把它当买卖信号回测,等于允许策略在波谷那一天精准买入、在见顶前一天精准卖出——收益必然严重虚高、参数寻优必然过拟合,回测结果与实盘表现脱节,**这正是本工具最要防的失真**。
+1 -1
View File
@@ -17,7 +17,7 @@ easy-tdx 要做的事很简单:**把机构的数据锁砸开,扔到每个普
**34个技术指标**MACD、KDJ、RSI、BOLL……连”捉妖大师”和”30日乖离率信号”都给你算好)开箱即用。
**缠论分析**(笔、中枢、买卖点、背驰)一键出结果——你不再需要手画分型、猜线段。
**内置回测引擎**——写个策略文件,一行命令跑回测,19 个经典策略自带,多因子组合、策略选股扫描,批量对比哪个最赚钱一目了然。**防过拟合验证链**(v1.25 新增)——Walk-Forward 七窗样本外验证(每窗独立开仓)、训练/验证/测试三段适配性体检(8 项可解释检查 + 「高适配」标记)、0-100 综合评分、多 seed 晋级门槛、买入持有基准对比,回测页勾选即出报告——「回测好」升级为「样本外也好」。**深度风险报告**(v1.28 新增)——绩效指标扩到 25 项(新增 Ulcer 指数、95% 日 VaR/CVaR、SQN 系统质量数、最大连胜/连亏);基准对比从超额收益一个数升级为 α/β/信息比率/跟踪误差四件套(「涨的时候跟不跟得上大盘、跌的时候抗不抗跌」一眼可读);策略一行带移动止损与百分比止损止盈(`self.buy(trail_stop=0.08)` 涨得越高止损线跟得越高,OCO 任一触发即失效);引擎行为由黄金测试锁定——19 个内置策略的指标基线进 CI,撮合/费率逻辑任何静默漂移当场爆红。
**内置回测引擎**——写个策略文件,一行命令跑回测,54 个经典策略自带(覆盖全部 50 个注册指标),多因子组合、策略选股扫描,批量对比哪个最赚钱一目了然。**防过拟合验证链**(v1.25 新增)——Walk-Forward 七窗样本外验证(每窗独立开仓)、训练/验证/测试三段适配性体检(8 项可解释检查 + 「高适配」标记)、0-100 综合评分、多 seed 晋级门槛、买入持有基准对比,回测页勾选即出报告——「回测好」升级为「样本外也好」。**深度风险报告**(v1.28 新增)——绩效指标扩到 25 项(新增 Ulcer 指数、95% 日 VaR/CVaR、SQN 系统质量数、最大连胜/连亏);基准对比从超额收益一个数升级为 α/β/信息比率/跟踪误差四件套(「涨的时候跟不跟得上大盘、跌的时候抗不抗跌」一眼可读);策略一行带移动止损与百分比止损止盈(`self.buy(trail_stop=0.08)` 涨得越高止损线跟得越高,OCO 任一触发即失效);引擎行为由黄金测试锁定——54 个内置策略的指标基线进 CI,撮合/费率逻辑任何静默漂移当场爆红。
**行情终端 Web UI**v1.23 重大升级)——`easy-tdx serve` 一条命令,浏览器秒变专业看盘终端:**市场看板**(五大指数实时推送 + 迷你分时、涨跌统计、四维情绪雷达、全市场涨跌分布直方图、涨停雷达、行业/概念热冷榜、涨幅/跌幅/成交额/换手四联排行榜、两市异动雷达)、**自选行情**(输入 6 位代码即加,全表 SSE 实时刷新、行内迷你分时)、**个股详情弹窗**(五档盘口 + 1/3/5 日分时 + 带 MA/BOLL/MACD/KDJ/RSI 指标切换的日 K,一键加自选、一键全策略寻优)、**板块下钻**(行业/概念弹窗看板块走势 + 成分股涨跌榜直达个股)。实时推送采用 SSE 单循环轮询 fan-out 架构(盘中 8 秒一拍,无人订阅自动休眠),自选持久化 SQLite。展示层设计对标专业终端(暗色、红涨绿跌、高信息密度),数据全部来自通达信协议直连——不花一分钱。
+1 -1
View File
@@ -640,7 +640,7 @@ easy-tdx serve --host 0.0.0.0</code></pre>
<h3>4.4 一键寻优所有策略(超实用)</h3>
<p>页面上还有一个橙色按钮 <strong>"一键寻优所有策略"</strong>。点它,系统会用每个策略自带的预设参数网格,把所有 18 个内置策略都跑一遍寻优,然后给你一个全局排名。</p>
<p>页面上还有一个橙色按钮 <strong>"一键寻优所有策略"</strong>。点它,系统会用每个策略自带的预设参数网格,把所有 54 个内置策略都跑一遍寻优,然后给你一个全局排名。</p>
<p>这对<strong>不知道用哪个策略的新手</strong>特别有用:让电脑告诉你,这个股票上哪个策略历史表现最好。</p>
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "easy-tdx"
version = "1.30.1"
version = "1.30.2"
description = "通达信 TCP 协议行情数据客户端,支持在线行情、离线数据读取与写入同步"
readme = "README.md"
requires-python = ">=3.10"
+206
View File
@@ -16,6 +16,8 @@
# V4.0 2026-06-02 handsomejustin 新增 ZHUOYAO,BIAS_SIGNAL两个自创函数
# V4.1 2026-06-14 新增 SAR(抛物线转向), VWAP(成交量加权均价), AROON(阿隆指标); 注册 FK
# V4.2 2026-07-09 新增 FSL(分水岭指标)
# V4.3 2026-09-03 新增无未来函数指标16个:HMA,KAMA,SUPERTREND,CHANDELIER,ICHIMOKU,
# UOS,CMO,TSI,FISHER,SQUEEZE,CHOP,AD,CMF,EFI,BBP,BBW(全部只引用当期及历史数据)
# 以下所有函数如无特别说明,输入参数S均为numpy序列或者列表list,N为整型int
# 应用层1级函数完美兼容通达信或同花顺,具体使用方法请参考通达信
@@ -571,4 +573,208 @@ def FSL(CLOSE, VOL, CAPITAL): # 分水岭指标:多空趋势强弱分界(SW
return RD(SWL), RD(SWS)
# ── V4.3 新增:无未来函数指标(只引用当期及历史数据,信号不漂移) ────────────
def HMA(S, N=16): # Hull 赫尔均线:WMA(2*WMA(N/2)-WMA(N), sqrt(N)),低滞后且不重绘
return WMA(2 * WMA(S, N // 2) - WMA(S, N), max(int(round(np.sqrt(N))), 1))
def KAMA(S, N=10, FAST=2, SLOW=30): # 考夫曼自适应均线:效率比驱动平滑系数(贴价/走平自适应)
S = np.asarray(S, dtype=float)
change = np.abs(S - REF(S, N)) # N 周期净变化
volatility = SUM(np.abs(DIFF(S)), N) # N 周期路径总波动
with np.errstate(divide="ignore", invalid="ignore"):
er = np.where(volatility > 0, change / volatility, 0.0) # 效率比 0~1
er = np.where(np.isnan(change) | np.isnan(volatility), np.nan, er) # 预热段保持 NaN
fast_sc = 2.0 / (FAST + 1)
slow_sc = 2.0 / (SLOW + 1)
sc = (er * (fast_sc - slow_sc) + slow_sc) ** 2 # 平滑系数(ER 高→贴价,ER 低→走平)
out = np.full(len(S), np.nan)
valid = np.flatnonzero(~np.isnan(sc))
if len(valid):
start = valid[0]
out[start] = S[start]
for i in range(start + 1, len(S)):
out[i] = out[i - 1] + sc[i] * (S[i] - out[i - 1])
return out
def SUPERTREND(CLOSE, HIGH, LOW, N=10, M=3.0): # 超级趋势:HL2±M*ATR 递推锁带,穿带翻多空
CLOSE = np.asarray(CLOSE, dtype=float)
HIGH = np.asarray(HIGH, dtype=float)
LOW = np.asarray(LOW, dtype=float)
n = len(CLOSE)
sar_like = np.full(n, np.nan)
if n == 0:
return sar_like, np.zeros(0, dtype=int)
# Wilder 平滑 ATR(与 TradingView ta.rma 同口径);首根 TR 用自身 H-L
prev_c = np.concatenate(([CLOSE[0]], CLOSE[:-1]))
tr = MAX(MAX(HIGH - LOW, ABS(HIGH - prev_c)), ABS(LOW - prev_c))
atr = np.empty(n)
atr[0] = tr[0]
for i in range(1, n):
atr[i] = atr[i - 1] + (tr[i] - atr[i - 1]) / N
hl2 = (HIGH + LOW) / 2
basic_up = hl2 + M * atr
basic_dn = hl2 - M * atr
# 递推锁带:带只朝价格方向收紧,反向放宽被前收盘穿越时重置
final_up = basic_up.copy()
final_dn = basic_dn.copy()
for i in range(1, n):
final_up[i] = (
basic_up[i]
if (basic_up[i] < final_up[i - 1] or CLOSE[i - 1] > final_up[i - 1])
else final_up[i - 1]
)
final_dn[i] = (
basic_dn[i]
if (basic_dn[i] > final_dn[i - 1] or CLOSE[i - 1] < final_dn[i - 1])
else final_dn[i - 1]
)
direction = np.ones(n, dtype=int) # 1=多头(ST=下轨),-1=空头(ST=上轨)
for i in range(1, n):
if direction[i - 1] == 1:
direction[i] = -1 if CLOSE[i] < final_dn[i] else 1
else:
direction[i] = 1 if CLOSE[i] > final_up[i] else -1
st = np.where(direction == 1, final_dn, final_up)
return st, direction
def CHANDELIER(CLOSE, HIGH, LOW, N=22, M=22, K=3.0): # 吊灯止损:多头 HHV-K*ATR / 空头 LLV+K*ATR
atr = ATR(CLOSE, HIGH, LOW, M)
long_stop = HHV(HIGH, N) - atr * K
short_stop = LLV(LOW, N) + atr * K
return long_stop, short_stop
def ICHIMOKU(HIGH, LOW, CLOSE, P1=9, P2=26, P3=52, SHIFT=26): # 一目均衡表:五线一云
tenkan = (HHV(HIGH, P1) + LLV(LOW, P1)) / 2 # 转换线
kijun = (HHV(HIGH, P2) + LLV(LOW, P2)) / 2 # 基准线
span_a = REF((tenkan + kijun) / 2, SHIFT) # 先行带A:SHIFT期前的值画到当前(仅过去数据)
span_b = REF((HHV(HIGH, P3) + LLV(LOW, P3)) / 2, SHIFT) # 先行带B
chikou = REF(CLOSE, -SHIFT) # 迟行带:当前收盘画回SHIFT期前,仅作图示(当期信号禁用)
return tenkan, kijun, span_a, span_b, chikou
def UOS(CLOSE, HIGH, LOW, P1=7, P2=14, P3=28, M=6): # 终极指标:7/14/28 三周期 BP/TR 加权动量
th = MAX(HIGH, REF(CLOSE, 1)) # 真高
tl = MIN(LOW, REF(CLOSE, 1)) # 真低
bp = CLOSE - tl # 买压
tr = th - tl # 真波幅
def _avg(p): # SMA(BP,p)/SMA(TR,p),分母为零(一字段)时取中性 0.5
num, den = MA(bp, p), MA(tr, p)
with np.errstate(divide="ignore", invalid="ignore"):
ratio = np.where(den > 0, num / den, 0.5)
return np.where(np.isnan(num) | np.isnan(den), np.nan, ratio)
uos = 100 * (4 * _avg(P1) + 2 * _avg(P2) + _avg(P3)) / 7
uos_ma = MA(uos, M)
return RD(uos), RD(uos_ma)
def CMO(CLOSE, N=14): # 钱德动量振荡器:(涨幅和-跌幅和)/总波幅*100,比RSI更锐利对称
dif = DIFF(CLOSE)
up = SUM(MAX(dif, 0), N)
dn = SUM(MAX(-dif, 0), N)
total = up + dn
with np.errstate(divide="ignore", invalid="ignore"):
cmo = np.where(total > 0, (up - dn) / total * 100, 0.0)
return RD(np.where(np.isnan(total), np.nan, cmo))
def TSI(CLOSE, R=25, S=13, M=13): # 真实强度指数:动量双重EMA平滑归一(William Blau
m = DIFF(CLOSE)
num = EMA(EMA(m, R), S)
den = EMA(EMA(ABS(m), R), S)
with np.errstate(divide="ignore", invalid="ignore"):
tsi = np.where(den != 0, 100 * num / den, 0.0)
tsi = np.where(np.isnan(num) | np.isnan(den), np.nan, tsi)
return RD(tsi), RD(EMA(tsi, M))
def FISHER(HIGH, LOW, N=9): # 费雪变换(John Ehlers):区间位置正态化,拐点尖锐
price = (HIGH + LOW) / 2.0
highest = HHV(price, N)
lowest = LLV(price, N)
rng = highest - lowest
with np.errstate(divide="ignore", invalid="ignore"):
raw = np.where(rng > 0, 2.0 * (price - lowest) / rng - 1.0, 0.0) # 归一化到 -1~1
norm = np.where(np.isnan(rng), np.nan, raw)
n = len(price)
value = np.full(n, np.nan)
fisher = np.full(n, np.nan)
for i in range(n):
if np.isnan(norm[i]):
continue
prev_v = 0.0 if np.isnan(value[i - 1]) else value[i - 1]
prev_f = 0.0 if np.isnan(fisher[i - 1]) else fisher[i - 1]
v = 0.33 * norm[i] + 0.67 * prev_v
v = max(-0.999, min(0.999, v)) # 钳制避免 ln 发散
value[i] = v
fisher[i] = 0.5 * np.log((1 + v) / (1 - v)) + 0.5 * prev_f
return fisher, REF(fisher, 1) # 第二返回值为触发线(前一期值)
def SQUEEZE(CLOSE, HIGH, LOW, N=20, BB=2.0, KC=1.5): # TTM 挤压动量:布林收进肯特纳,回归动量定方向
mid = MA(CLOSE, N)
dev = STD(CLOSE, N) * BB
tr = MAX(MAX(HIGH - LOW, ABS(HIGH - REF(CLOSE, 1))), ABS(LOW - REF(CLOSE, 1)))
range_ma = MA(tr, N)
# 挤压判定:布林带(±dev)整体落在肯特纳通道(±KC×range_ma)内部(NaN 预热期比较为 False)
sqz_on = dev < KC * range_ma
# 动量:中价偏离 (HH+LL)/2 与 SMA(C,N) 的均值,取线性回归当前拟合值
delta = CLOSE - ((HHV(HIGH, N) + LLV(LOW, N)) / 2 + mid) / 2
mom = FORCAST(delta, N)
return sqz_on, RD(mom)
def CHOP(HIGH, LOW, CLOSE, N=14): # 盘整指数:>61.8 震荡 / <38.2 趋势(ΣTR/区间 对数缩放到 0~100)
tr = MAX(MAX(HIGH - LOW, ABS(HIGH - REF(CLOSE, 1))), ABS(LOW - REF(CLOSE, 1)))
tr_sum = SUM(tr, N)
rng = HHV(HIGH, N) - LLV(LOW, N)
with np.errstate(divide="ignore", invalid="ignore"):
ratio = np.where((tr_sum > 0) & (rng > 0), tr_sum / rng, np.nan)
return 100 * np.log10(ratio) / np.log10(N)
def AD(CLOSE, HIGH, LOW, VOL): # 累积/派发线(Marc Chaikin):CLV*VOL 累计,OBV 的精细化版
rng = HIGH - LOW
with np.errstate(divide="ignore", invalid="ignore"):
clv = np.where(rng > 0, ((CLOSE - LOW) - (HIGH - CLOSE)) / rng, 0.0) # 收盘位置 -1~1
return SUM(clv * VOL, 0)
def CMF(CLOSE, HIGH, LOW, VOL, N=20): # 佳庆资金流量:N日 CLV*VOL 之和 / N日成交量之和
rng = HIGH - LOW
with np.errstate(divide="ignore", invalid="ignore"):
clv = np.where(rng > 0, ((CLOSE - LOW) - (HIGH - CLOSE)) / rng, 0.0)
mfv_sum = SUM(clv * VOL, N)
vol_sum = SUM(VOL, N)
with np.errstate(divide="ignore", invalid="ignore"):
cmf = np.where(vol_sum > 0, mfv_sum / vol_sum, 0.0)
return np.where(np.isnan(vol_sum) | np.isnan(mfv_sum), np.nan, cmf)
def EFI(CLOSE, VOL, N=13): # 艾尔德强力指数:EMA(ΔC×V),同时融合方向/幅度/成交量三维
return EMA(DIFF(CLOSE) * VOL, N)
def BBP(CLOSE, N=20, P=2): # 布林位置 %B:收盘在带内位置百分比(0=下轨,50=中轨,100=上轨)
mid = MA(CLOSE, N)
dev = STD(CLOSE, N) * P
width = 2 * dev
with np.errstate(divide="ignore", invalid="ignore"):
pos = np.where(width > 0, (CLOSE - (mid - dev)) / width * 100, 50.0)
return RD(np.where(np.isnan(width), np.nan, pos))
def BBW(CLOSE, N=20, P=2): # 布林带宽:(上轨-下轨)/中轨*100,收窄=波动挤压/变盘预警
mid = MA(CLOSE, N)
dev = STD(CLOSE, N) * P
with np.errstate(divide="ignore", invalid="ignore"):
width = np.where(mid > 0, 2 * dev / mid * 100, np.nan)
return RD(width)
# 望大家能提交更多指标和函数 https://github.com/mpquant/MyTT
+70 -1
View File
@@ -110,7 +110,76 @@ def FSL(
VOL: npt.ArrayLike,
CAPITAL: float,
) -> tuple[NDArray, NDArray]: ...
def ZIG(S: npt.ArrayLike, X: float = ...) -> NDArray: ...
# ── V4.3 无未来函数指标 ─────────────────────────────────────────────────────
def HMA(S: npt.ArrayLike, N: int = ...) -> NDArray: ...
def KAMA(S: npt.ArrayLike, N: int = ..., FAST: int = ..., SLOW: int = ...) -> NDArray: ...
def SUPERTREND(
CLOSE: npt.ArrayLike,
HIGH: npt.ArrayLike,
LOW: npt.ArrayLike,
N: int = ...,
M: float = ...,
) -> tuple[NDArray, npt.NDArray[np.int32]]: ...
def CHANDELIER(
CLOSE: npt.ArrayLike,
HIGH: npt.ArrayLike,
LOW: npt.ArrayLike,
N: int = ...,
M: int = ...,
K: float = ...,
) -> tuple[NDArray, NDArray]: ...
def ICHIMOKU(
HIGH: npt.ArrayLike,
LOW: npt.ArrayLike,
CLOSE: npt.ArrayLike,
P1: int = ...,
P2: int = ...,
P3: int = ...,
SHIFT: int = ...,
) -> tuple[NDArray, NDArray, NDArray, NDArray, NDArray]: ...
def UOS(
CLOSE: npt.ArrayLike,
HIGH: npt.ArrayLike,
LOW: npt.ArrayLike,
P1: int = ...,
P2: int = ...,
P3: int = ...,
M: int = ...,
) -> tuple[NDArray, NDArray]: ...
def CMO(CLOSE: npt.ArrayLike, N: int = ...) -> NDArray: ...
def TSI(
CLOSE: npt.ArrayLike,
R: int = ...,
S: int = ...,
M: int = ...,
) -> tuple[NDArray, NDArray]: ...
def FISHER(
HIGH: npt.ArrayLike,
LOW: npt.ArrayLike,
N: int = ...,
) -> tuple[NDArray, NDArray]: ...
def SQUEEZE(
CLOSE: npt.ArrayLike,
HIGH: npt.ArrayLike,
LOW: npt.ArrayLike,
N: int = ...,
BB: float = ...,
KC: float = ...,
) -> tuple[npt.NDArray[np.bool_], NDArray]: ...
def CHOP(HIGH: npt.ArrayLike, LOW: npt.ArrayLike, CLOSE: npt.ArrayLike, N: int = ...) -> NDArray: ...
def AD(CLOSE: npt.ArrayLike, HIGH: npt.ArrayLike, LOW: npt.ArrayLike, VOL: npt.ArrayLike) -> NDArray: ...
def CMF(
CLOSE: npt.ArrayLike,
HIGH: npt.ArrayLike,
LOW: npt.ArrayLike,
VOL: npt.ArrayLike,
N: int = ...,
) -> NDArray: ...
def EFI(CLOSE: npt.ArrayLike, VOL: npt.ArrayLike, N: int = ...) -> NDArray: ...
def BBP(CLOSE: npt.ArrayLike, N: int = ..., P: float = ...) -> NDArray: ...
def BBW(CLOSE: npt.ArrayLike, N: int = ..., P: float = ...) -> NDArray: ...
# ── Utility Functions ────────────────────────────────────────────────────────
File diff suppressed because it is too large Load Diff
+91
View File
@@ -239,6 +239,97 @@ _reg(
"FK 趋势指标(EMA(2) 突破斜率外推 EMA(42),动量偏离检测)",
)
# ── V4.3 无未来函数指标(全部只引用当期及历史数据,信号不漂移) ──────────
# 趋势/止损类
_reg(
"SUPERTREND",
("close", "high", "low"),
("SUPERTREND", "ST_DIR"),
MyTT.SUPERTREND,
{"N": 10, "M": 3.0},
"SUPERTREND 超级趋势(ATR 通道趋势跟踪,兼移动止损;ST_DIR=1多/-1空)",
)
_reg(
"CHANDELIER",
("close", "high", "low"),
("CHDL_LONG", "CHDL_SHORT"),
MyTT.CHANDELIER,
{"N": 22, "M": 22, "K": 3.0},
"CHANDELIER 吊灯止损(多头/空头 ATR 跟踪止损位)",
)
_reg("HMA", ("close",), ("HMA",), MyTT.HMA, {"N": 16}, "HMA 赫尔均线(低滞后不重绘)")
_reg(
"KAMA",
("close",),
("KAMA",),
MyTT.KAMA,
{"N": 10, "FAST": 2, "SLOW": 30},
"KAMA 考夫曼自适应均线(趋势市贴价/震荡市走平)",
)
_reg(
"ICHIMOKU",
("high", "low", "close"),
("IC_TENKAN", "IC_KIJUN", "IC_SPAN_A", "IC_SPAN_B", "IC_CHIKOU"),
MyTT.ICHIMOKU,
{"P1": 9, "P2": 26, "P3": 52, "SHIFT": 26},
"ICHIMOKU 一目均衡表(迟行带 IC_CHIKOU 仅作图示,当期信号请用转换/基准线与先行带)",
)
# 动量/振荡类
_reg(
"UOS",
("close", "high", "low"),
("UOS", "UOS_MA"),
MyTT.UOS,
{"P1": 7, "P2": 14, "P3": 28, "M": 6},
"UOS 终极指标(7/14/28 三周期加权动量)",
)
_reg("CMO", ("close",), ("CMO",), MyTT.CMO, {"N": 14}, "CMO 钱德动量振荡器")
_reg(
"TSI", ("close",), ("TSI", "TSI_MA"), MyTT.TSI, {"R": 25, "S": 13, "M": 13}, "TSI 真实强度指数"
)
_reg(
"FISHER",
("high", "low"),
("FISHER", "FISHER_TRG"),
MyTT.FISHER,
{"N": 9},
"FISHER 费雪变换(区间位置正态化,拐点尖锐)",
)
# 波动率/状态识别类
_reg(
"SQUEEZE",
("close", "high", "low"),
("SQZ_ON", "SQZ_MOM"),
MyTT.SQUEEZE,
{"N": 20, "BB": 2.0, "KC": 1.5},
"SQUEEZE TTM 挤压动量(布林收进肯特纳=挤压态,SQZ_MOM 为线性回归动量)",
)
_reg(
"CHOP",
("close", "high", "low"),
("CHOP",),
MyTT.CHOP,
{"N": 14},
"CHOP 盘整指数(>61.8 震荡 / <38.2 趋势)",
)
_reg(
"BBP",
("close",),
("BBP",),
MyTT.BBP,
{"N": 20, "P": 2},
"BBP 布林位置 %B(0=下轨,50=中轨,100=上轨)",
)
_reg("BBW", ("close",), ("BBW",), MyTT.BBW, {"N": 20, "P": 2}, "BBW 布林带宽(收窄=变盘预警)")
# 量能/资金类
_reg("AD", ("close", "high", "low", "vol"), ("AD",), MyTT.AD, {}, "AD 累积/派发线")
_reg("CMF", ("close", "high", "low", "vol"), ("CMF",), MyTT.CMF, {"N": 20}, "CMF 佳庆资金流量")
_reg("EFI", ("close", "vol"), ("EFI",), MyTT.EFI, {"N": 13}, "EFI 艾尔德强力指数")
def list_indicators() -> list[dict[str, object]]:
"""返回所有可用指标的元数据。"""
+455
View File
@@ -90,6 +90,45 @@
}
},
"strategies": {
"ad_cross": {
"cvar_95": 0.024286411493930567,
"max_consecutive_losses": 2,
"max_consecutive_wins": 7,
"max_drawdown": 0.12904549392252831,
"sharpe": 1.413959529316044,
"sqn": 1.9143440657904827,
"total_return": 0.5433184647040252,
"total_trades": 16,
"ulcer_index": 0.04114422319311987,
"var_95": 0.018706317214833772,
"win_rate": 0.75
},
"aroon_cross": {
"cvar_95": 0.029266225981937833,
"max_consecutive_losses": 1,
"max_consecutive_wins": 1,
"max_drawdown": 0.23484266406886486,
"sharpe": -0.1318253497365565,
"sqn": -0.02853515598272609,
"total_return": -0.026907343025617925,
"total_trades": 8,
"ulcer_index": 0.11536620900391767,
"var_95": 0.02282875179249166,
"win_rate": 0.5
},
"asi_cross": {
"cvar_95": 0.02981959329970092,
"max_consecutive_losses": 3,
"max_consecutive_wins": 7,
"max_drawdown": 0.10955462389011326,
"sharpe": 0.7869328181478238,
"sqn": 1.285955774339363,
"total_return": 0.31630572526536627,
"total_trades": 37,
"ulcer_index": 0.051165506301142354,
"var_95": 0.020699239050410923,
"win_rate": 0.6216216216216216
},
"atr_breakout": {
"cvar_95": 0.029816014464276553,
"max_consecutive_losses": 2,
@@ -116,6 +155,32 @@
"var_95": 0.02185464362709669,
"win_rate": 0.43243243243243246
},
"bbp_reversal": {
"cvar_95": 0.025123143703972416,
"max_consecutive_losses": 1,
"max_consecutive_wins": 2,
"max_drawdown": 0.12529045393048568,
"sharpe": -0.08270429911478913,
"sqn": 0.984709712259246,
"total_return": 0.00949379410277551,
"total_trades": 3,
"ulcer_index": 0.039203351555811915,
"var_95": 0.015350573492733575,
"win_rate": 0.6666666666666666
},
"bbw_squeeze": {
"cvar_95": 0.024332883581541946,
"max_consecutive_losses": 6,
"max_consecutive_wins": 3,
"max_drawdown": 0.15870878255389867,
"sharpe": -0.4216428614980978,
"sqn": -0.5443884480781064,
"total_return": -0.059449894173721374,
"total_trades": 14,
"ulcer_index": 0.07851315060195874,
"var_95": 0.014460430360468098,
"win_rate": 0.35714285714285715
},
"bias_reversal": {
"cvar_95": 0.031800618199536355,
"max_consecutive_losses": 4,
@@ -129,6 +194,19 @@
"var_95": 0.02439437484336824,
"win_rate": 0.5111111111111111
},
"bias_signal_cross": {
"cvar_95": 0.02748879702227868,
"max_consecutive_losses": 4,
"max_consecutive_wins": 3,
"max_drawdown": 0.2938157158635232,
"sharpe": -0.17985871928985922,
"sqn": -0.03800371473682254,
"total_return": -0.03295246594055856,
"total_trades": 10,
"ulcer_index": 0.1651695141252374,
"var_95": 0.021475132232438594,
"win_rate": 0.5
},
"boll_breakout": {
"cvar_95": 0.025123143703972416,
"max_consecutive_losses": 1,
@@ -142,6 +220,19 @@
"var_95": 0.015350573492733575,
"win_rate": 0.6666666666666666
},
"brar_reversal": {
"cvar_95": -0.0,
"max_consecutive_losses": 0,
"max_consecutive_wins": 0,
"max_drawdown": 0.0,
"sharpe": 0.0,
"sqn": 0.0,
"total_return": 0.0,
"total_trades": 0,
"ulcer_index": 0.0,
"var_95": -0.0,
"win_rate": 0.0
},
"cci": {
"cvar_95": 0.028065506761634905,
"max_consecutive_losses": 1,
@@ -155,6 +246,84 @@
"var_95": 0.019114854583397116,
"win_rate": 0.6363636363636364
},
"chandelier": {
"cvar_95": -0.0,
"max_consecutive_losses": 0,
"max_consecutive_wins": 0,
"max_drawdown": 0.0,
"sharpe": 0.0,
"sqn": 0.0,
"total_return": 0.0,
"total_trades": 0,
"ulcer_index": 0.0,
"var_95": -0.0,
"win_rate": 0.0
},
"chop_trend": {
"cvar_95": 0.016521228683029757,
"max_consecutive_losses": 2,
"max_consecutive_wins": 1,
"max_drawdown": 0.1197429207500985,
"sharpe": 0.23777478167962413,
"sqn": 0.8450262324842597,
"total_return": 0.08186349084625855,
"total_trades": 4,
"ulcer_index": 0.02939434967310675,
"var_95": 0.007369600793270218,
"win_rate": 0.5
},
"cmf_zero": {
"cvar_95": 0.024141735522224646,
"max_consecutive_losses": 2,
"max_consecutive_wins": 5,
"max_drawdown": 0.14344144457956093,
"sharpe": 0.9133320420008789,
"sqn": 1.5782396113650308,
"total_return": 0.32189557881670994,
"total_trades": 20,
"ulcer_index": 0.05122369616579118,
"var_95": 0.0180655722025719,
"win_rate": 0.7
},
"cmo_reversal": {
"cvar_95": 0.026729960510057743,
"max_consecutive_losses": 1,
"max_consecutive_wins": 2,
"max_drawdown": 0.16495853483792264,
"sharpe": 0.6275440425539472,
"sqn": 1.8403059687214964,
"total_return": 0.2359516039506322,
"total_trades": 3,
"ulcer_index": 0.06452564310085397,
"var_95": 0.01934233590561444,
"win_rate": 0.6666666666666666
},
"cr_reversal": {
"cvar_95": 0.02369389369181201,
"max_consecutive_losses": 0,
"max_consecutive_wins": 0,
"max_drawdown": 0.12556824942616718,
"sharpe": 0.13618190785650225,
"sqn": 0.0,
"total_return": 0.06417970182278632,
"total_trades": 0,
"ulcer_index": 0.023595036704376637,
"var_95": 0.01255926432583018,
"win_rate": 0.0
},
"dfma_cross": {
"cvar_95": 0.0258934274012887,
"max_consecutive_losses": 4,
"max_consecutive_wins": 2,
"max_drawdown": 0.22430138432687624,
"sharpe": -0.2451029943989093,
"sqn": -0.21296181317887017,
"total_return": -0.047787872823673916,
"total_trades": 12,
"ulcer_index": 0.12547066912166988,
"var_95": 0.02065490866252133,
"win_rate": 0.4166666666666667
},
"dmi": {
"cvar_95": 0.029255190955404537,
"max_consecutive_losses": 5,
@@ -194,6 +363,19 @@
"var_95": 0.019685727992254924,
"win_rate": 0.45
},
"efi_zero": {
"cvar_95": 0.029368306602455728,
"max_consecutive_losses": 6,
"max_consecutive_wins": 3,
"max_drawdown": 0.12386834887621843,
"sharpe": 0.1310163040019758,
"sqn": 0.354718917262971,
"total_return": 0.058226194433868006,
"total_trades": 29,
"ulcer_index": 0.06909264613158413,
"var_95": 0.022919408952405854,
"win_rate": 0.3793103448275862
},
"ema_cross": {
"cvar_95": 0.030512787117540286,
"max_consecutive_losses": 5,
@@ -220,6 +402,45 @@
"var_95": 0.02157076164724241,
"win_rate": 0.32142857142857145
},
"expma_cross": {
"cvar_95": 0.032018722856295576,
"max_consecutive_losses": 4,
"max_consecutive_wins": 1,
"max_drawdown": 0.3291585898907232,
"sharpe": -1.1723060706959245,
"sqn": -2.9049331578730926,
"total_return": -0.30675148747892034,
"total_trades": 7,
"ulcer_index": 0.18255002434586737,
"var_95": 0.024497075615117908,
"win_rate": 0.14285714285714285
},
"fisher_cross": {
"cvar_95": 0.027142953781316027,
"max_consecutive_losses": 4,
"max_consecutive_wins": 6,
"max_drawdown": 0.13242479869280652,
"sharpe": 0.7747655122625644,
"sqn": 1.2258640743172122,
"total_return": 0.3000150148326932,
"total_trades": 39,
"ulcer_index": 0.050503189087610034,
"var_95": 0.019196177653336477,
"win_rate": 0.5128205128205128
},
"fk_reversal": {
"cvar_95": 0.027673675923482476,
"max_consecutive_losses": 3,
"max_consecutive_wins": 3,
"max_drawdown": 0.1374258713555942,
"sharpe": 0.05310588929298746,
"sqn": 0.2609297927675816,
"total_return": 0.03098826668855792,
"total_trades": 19,
"ulcer_index": 0.06681780689649293,
"var_95": 0.021675988751594637,
"win_rate": 0.42105263157894735
},
"fsl": {
"cvar_95": 0.028932815510588083,
"max_consecutive_losses": 5,
@@ -233,6 +454,45 @@
"var_95": 0.022744109351729155,
"win_rate": 0.35714285714285715
},
"hma_cross": {
"cvar_95": 0.02601608749612084,
"max_consecutive_losses": 4,
"max_consecutive_wins": 5,
"max_drawdown": 0.14013863361462378,
"sharpe": 0.9018510804449479,
"sqn": 1.3059591751970854,
"total_return": 0.32817705633540206,
"total_trades": 19,
"ulcer_index": 0.0731080920454242,
"var_95": 0.0191409819205396,
"win_rate": 0.631578947368421
},
"ichimoku_cross": {
"cvar_95": 0.030953331370946267,
"max_consecutive_losses": 1,
"max_consecutive_wins": 1,
"max_drawdown": 0.21803844858131277,
"sharpe": -0.1390386894303238,
"sqn": -0.09653804098207303,
"total_return": -0.02512157114818303,
"total_trades": 3,
"ulcer_index": 0.08946241642268053,
"var_95": 0.023037962387550195,
"win_rate": 0.3333333333333333
},
"kama_cross": {
"cvar_95": 0.029047612117214912,
"max_consecutive_losses": 4,
"max_consecutive_wins": 3,
"max_drawdown": 0.12857725100336337,
"sharpe": 0.32104883660224726,
"sqn": 0.7764180118662009,
"total_return": 0.12589848610890342,
"total_trades": 28,
"ulcer_index": 0.06905462712627519,
"var_95": 0.022124546340830314,
"win_rate": 0.42857142857142855
},
"kdj_cross": {
"cvar_95": 0.027898201822013535,
"max_consecutive_losses": 3,
@@ -285,6 +545,84 @@
"var_95": 0.02189769434012649,
"win_rate": 0.35294117647058826
},
"mass_cross": {
"cvar_95": 0.03054297901767067,
"max_consecutive_losses": 2,
"max_consecutive_wins": 3,
"max_drawdown": 0.18266006667782547,
"sharpe": -0.14300372942671147,
"sqn": -0.00020527251510869655,
"total_return": -0.027331098242567076,
"total_trades": 19,
"ulcer_index": 0.09032412136723328,
"var_95": 0.02275460045342278,
"win_rate": 0.5789473684210527
},
"mfi_reversal": {
"cvar_95": 0.03034753275884507,
"max_consecutive_losses": 0,
"max_consecutive_wins": 2,
"max_drawdown": 0.2549569044138692,
"sharpe": 0.44061001264676536,
"sqn": 2.139161183716703,
"total_return": 0.1876926316446088,
"total_trades": 2,
"ulcer_index": 0.13667491572629412,
"var_95": 0.02415090052701228,
"win_rate": 1.0
},
"mtm_cross": {
"cvar_95": 0.02750195617227611,
"max_consecutive_losses": 4,
"max_consecutive_wins": 4,
"max_drawdown": 0.11827463808180305,
"sharpe": 0.3325814486269132,
"sqn": 0.7509810732371223,
"total_return": 0.12604681721555666,
"total_trades": 41,
"ulcer_index": 0.06304678610789029,
"var_95": 0.019735845693466002,
"win_rate": 0.5365853658536586
},
"obv_cross": {
"cvar_95": 0.027762055897527498,
"max_consecutive_losses": 5,
"max_consecutive_wins": 3,
"max_drawdown": 0.23680264438544252,
"sharpe": -0.5559242026190786,
"sqn": -0.5854574307963847,
"total_return": -0.14813287593396007,
"total_trades": 28,
"ulcer_index": 0.12848631836406813,
"var_95": 0.02210768744086329,
"win_rate": 0.42857142857142855
},
"psy_reversal": {
"cvar_95": 0.02891784823229219,
"max_consecutive_losses": 2,
"max_consecutive_wins": 0,
"max_drawdown": 0.16281839827998396,
"sharpe": -0.1335645556612112,
"sqn": -3.036213974648354,
"total_return": -0.02547860131516233,
"total_trades": 2,
"ulcer_index": 0.07622136686886101,
"var_95": 0.02124766218780112,
"win_rate": 0.0
},
"roc_zero": {
"cvar_95": 0.028767825863300883,
"max_consecutive_losses": 7,
"max_consecutive_wins": 4,
"max_drawdown": 0.18493301021337408,
"sharpe": -0.31536772495096077,
"sqn": -0.4545879597619188,
"total_return": -0.08338098843843589,
"total_trades": 29,
"ulcer_index": 0.09798341426090898,
"var_95": 0.0218502235858578,
"win_rate": 0.3103448275862069
},
"rsi_reversal": {
"cvar_95": 0.030776659581515254,
"max_consecutive_losses": 1,
@@ -298,6 +636,45 @@
"var_95": 0.024060510820245292,
"win_rate": 0.5
},
"sar_follow": {
"cvar_95": 0.02857107698245897,
"max_consecutive_losses": 4,
"max_consecutive_wins": 3,
"max_drawdown": 0.1898358700030981,
"sharpe": -0.19331831081777617,
"sqn": -0.10641340723878887,
"total_return": -0.04272895317218284,
"total_trades": 18,
"ulcer_index": 0.09655157548485278,
"var_95": 0.021467949126669807,
"win_rate": 0.5
},
"squeeze_breakout": {
"cvar_95": 0.024742251658719575,
"max_consecutive_losses": 3,
"max_consecutive_wins": 1,
"max_drawdown": 0.21733278570085734,
"sharpe": -0.9308991323778134,
"sqn": -1.0480133746814557,
"total_return": -0.14111036967684343,
"total_trades": 4,
"ulcer_index": 0.1320941903542238,
"var_95": 0.013420945706258479,
"win_rate": 0.25
},
"supertrend": {
"cvar_95": 0.03072730738114505,
"max_consecutive_losses": 5,
"max_consecutive_wins": 1,
"max_drawdown": 0.28689785114697847,
"sharpe": -0.4953361337737291,
"sqn": -0.7107356522879448,
"total_return": -0.13704473609360412,
"total_trades": 9,
"ulcer_index": 0.1646826003824428,
"var_95": 0.02294677360521743,
"win_rate": 0.2222222222222222
},
"triple_ma": {
"cvar_95": 0.030748053405195853,
"max_consecutive_losses": 2,
@@ -324,6 +701,58 @@
"var_95": 0.021929213323307422,
"win_rate": 0.5
},
"tsi_cross": {
"cvar_95": 0.028194974350123875,
"max_consecutive_losses": 7,
"max_consecutive_wins": 2,
"max_drawdown": 0.17090407134804525,
"sharpe": -0.4846436640713545,
"sqn": -0.7878323795262339,
"total_return": -0.13001127539228385,
"total_trades": 15,
"ulcer_index": 0.10515558844327079,
"var_95": 0.022322699465880603,
"win_rate": 0.3333333333333333
},
"uos_reversal": {
"cvar_95": 0.030193757775442014,
"max_consecutive_losses": 0,
"max_consecutive_wins": 1,
"max_drawdown": 0.16294320323774955,
"sharpe": 0.44386948720403563,
"sqn": 0.0,
"total_return": 0.1801330140672519,
"total_trades": 1,
"ulcer_index": 0.06537062768285902,
"var_95": 0.02347496839245481,
"win_rate": 1.0
},
"vr_reversal": {
"cvar_95": 0.0004713577549215952,
"max_consecutive_losses": 0,
"max_consecutive_wins": 1,
"max_drawdown": 0.06396617092173154,
"sharpe": -0.03368193863407347,
"sqn": 0.0,
"total_return": 0.04095785734355717,
"total_trades": 1,
"ulcer_index": 0.009850103065533909,
"var_95": -0.0,
"win_rate": 1.0
},
"vwap_cross": {
"cvar_95": 0.029302922491254713,
"max_consecutive_losses": 8,
"max_consecutive_wins": 3,
"max_drawdown": 0.16046654742962813,
"sharpe": -0.009907074763182101,
"sqn": 0.1445122693129962,
"total_return": 0.011710672435813363,
"total_trades": 21,
"ulcer_index": 0.08439886174470289,
"var_95": 0.022126808733294197,
"win_rate": 0.38095238095238093
},
"wr_reversal": {
"cvar_95": -0.0,
"max_consecutive_losses": 0,
@@ -336,6 +765,32 @@
"ulcer_index": 0.0,
"var_95": -0.0,
"win_rate": 0.0
},
"xsii_breakout": {
"cvar_95": 0.02864695342561932,
"max_consecutive_losses": 3,
"max_consecutive_wins": 3,
"max_drawdown": 0.1495877433127484,
"sharpe": -0.09431845461633159,
"sqn": 0.14548639609968736,
"total_return": -0.013129418686880778,
"total_trades": 16,
"ulcer_index": 0.07348301348613494,
"var_95": 0.021802116298984472,
"win_rate": 0.4375
},
"zhuoyao_trend": {
"cvar_95": 0.030356181136770456,
"max_consecutive_losses": 2,
"max_consecutive_wins": 2,
"max_drawdown": 0.25518084654756473,
"sharpe": -0.574715368538664,
"sqn": -1.2971641853791143,
"total_return": -0.12930241173833024,
"total_trades": 5,
"ulcer_index": 0.13655713750977927,
"var_95": 0.021555547544702937,
"win_rate": 0.4
}
}
}
+2 -2
View File
@@ -3,7 +3,7 @@
核心保证:**同一 df + 同参数下,向量化路径与逐 bar 路径的输出逐位一致**
performance / trades / equity_curve / positions 全比对)。
- 对拍覆盖:全部 19 个内置策略(默认参数)+ ma_cross/macd/boll/rsi 的非默认
- 对拍覆盖:全部内置策略(默认参数,当前 54 个+ ma_cross/macd/boll/rsi 的非默认
参数组合 + warmup / 极低资金(买不足 1 手的退化路径)/ 非默认费率与成交价模式;
- 约束检测:``_vectorize_eligibility`` 的显式约束(无掩码 / 缠论注入)与
``signal_path`` 的 auto/vector/loop 语义。
@@ -115,7 +115,7 @@ def _run_both(
return loop, vec
# ── 对拍:19 个内置策略 × 默认参数 ───────────────────────────────────────────
# ── 对拍:全部内置策略 × 默认参数 ───────────────────────────────────────────
#: 已知「默认参数下不会交易」的策略:MyTT 的 WR 是 0~100 刻度(100=超卖),
+344 -1
View File
@@ -1,4 +1,4 @@
"""MyTT.py 新增指标函数(SAR/VWAP/AROON/FK)的数值正确性与边界测试。
"""MyTT.py 新增指标函数(SAR/VWAP/AROON/FK + V4.3 十六个无未来函数指标)的数值正确性与边界测试。
这些测试针对 MyTT.py 里函数本身,不经过 indicator.py 注册层。
注册层的端到端覆盖在 test_indicator.py::TestComputeIndicators::test_all_registered_indicators_run。
@@ -214,3 +214,346 @@ class TestFK:
close = np.array([100 - i for i in range(100)], dtype=float)
fk = MyTT.FK(close)
assert bool(fk[-1]) is True
# ═══════════════════════════════════════════════════════════════════════════
# V4.3 新增:16 个无未来函数指标
# ═══════════════════════════════════════════════════════════════════════════
#: 新指标的构造器:统一接收 (open, high, low, close, vol) 五元组,返回输出元组。
#: 用于「无未来函数」前缀一致性回归(见 TestNoLookahead)。
_V43_INDICATORS = {
"HMA": lambda o, h, lo, c, v: (MyTT.HMA(c, 16),),
"KAMA": lambda o, h, lo, c, v: (MyTT.KAMA(c),),
"SUPERTREND": lambda o, h, lo, c, v: MyTT.SUPERTREND(c, h, lo),
"CHANDELIER": lambda o, h, lo, c, v: MyTT.CHANDELIER(c, h, lo),
"ICHIMOKU": lambda o, h, lo, c, v: MyTT.ICHIMOKU(h, lo, c),
"UOS": lambda o, h, lo, c, v: MyTT.UOS(c, h, lo),
"CMO": lambda o, h, lo, c, v: (MyTT.CMO(c),),
"TSI": lambda o, h, lo, c, v: MyTT.TSI(c),
"FISHER": lambda o, h, lo, c, v: MyTT.FISHER(h, lo),
"SQUEEZE": lambda o, h, lo, c, v: MyTT.SQUEEZE(c, h, lo),
"CHOP": lambda o, h, lo, c, v: (MyTT.CHOP(h, lo, c),),
"AD": lambda o, h, lo, c, v: (MyTT.AD(c, h, lo, v),),
"CMF": lambda o, h, lo, c, v: (MyTT.CMF(c, h, lo, v),),
"EFI": lambda o, h, lo, c, v: (MyTT.EFI(c, v),),
"BBP": lambda o, h, lo, c, v: (MyTT.BBP(c),),
"BBW": lambda o, h, lo, c, v: (MyTT.BBW(c),),
}
class TestNoLookahead:
"""无未来函数回归:指标在全序列上前缀段输出 == 仅用前缀数据计算的输出。
未来函数(如 ZIG)的致命特征是:后到的数据会改写历史输出。本测试
把 200 根 K 线截断到前 120 根,两组输出在重叠段必须逐位一致——
任何引用了 t+1 及之后数据的实现都会当场爆红。
"""
PREFIX = 120
@pytest.mark.parametrize("name", sorted(_V43_INDICATORS))
def test_prefix_stability(self, name):
ohlcv = _ohlcv(200)
full = _V43_INDICATORS[name](*ohlcv)
part = _V43_INDICATORS[name](*[x[: self.PREFIX] for x in ohlcv])
for j, (f, p) in enumerate(zip(full, part)):
# ICHIMOKU 迟行带引用未来数据画图(文档已声明仅作图示),
# 它是唯一允许前缀不一致的输出,单独跳过(见 TestICHIMOKU)。
if name == "ICHIMOKU" and j == 4:
continue
assert np.allclose(f[: self.PREFIX], p, equal_nan=True), (
f"{name} 输出#{j} 前缀不一致:疑似引用了未来数据"
)
class TestHMA:
def test_warmup_and_length(self):
close = _ohlcv()[3]
hma = MyTT.HMA(close, 16)
assert len(hma) == len(close)
assert np.isnan(hma[:14]).all() # 最内层 WMA(16) 窗口预热
assert not np.isnan(hma[19])
def test_rising_market_follows_price(self):
close = np.arange(100, dtype=float) * 0.5 + 10
hma = MyTT.HMA(close, 16)
# 单边上涨中低滞后均线应贴在价格下方且不发散
assert (hma[20:] <= close[20:] + 1e-6).all()
assert hma[-1] > close[-2] # 跟随上涨
class TestKAMA:
def test_warmup_starts_at_n(self):
close = _ohlcv()[3]
kama = MyTT.KAMA(close, N=10)
assert np.isnan(kama[:10]).all()
assert not np.isnan(kama[10])
def test_strong_trend_hugs_price(self):
# 单边强趋势:效率比≈1,KAMA 平滑系数取快速极值,稳态滞后 ≈ (1-sc)/sc ≈ 1.25 根
close = np.cumsum(np.ones(100)) # 每根 +1 的完美趋势
kama = MyTT.KAMA(close, N=10)
assert np.abs(kama[-1] - close[-1]) < 2.0
def test_flat_market_flat_kama(self):
kama = MyTT.KAMA(np.full(60, 10.0), N=10)
assert np.allclose(kama[10:], 10.0)
class TestSUPERTREND:
def test_direction_values(self):
_, high, low, close, _ = _ohlcv()
st, direction = MyTT.SUPERTREND(close, high, low)
assert set(np.unique(direction).tolist()) <= {1, -1}
assert np.isfinite(st).all()
def test_rising_market_st_below_price(self):
# 持续上涨:趋势为多,ST(下轨)应持续低于最低价
high = np.arange(80, dtype=float) + 1
low = np.arange(80, dtype=float)
close = high.copy()
st, direction = MyTT.SUPERTREND(close, high, low, N=10, M=3.0)
assert direction[-1] == 1
assert (st[10:] <= low[10:] + 1e-6).all()
def test_reversal_flips_direction(self):
# V 型反转:方向必须从 1 翻到 -1
half = np.arange(40, dtype=float)
close = np.concatenate([half + 1, 40 - half])
high = close + 0.5
low = close - 0.5
_, direction = MyTT.SUPERTREND(close, high, low)
assert direction[0] != direction[-1]
def test_empty_input(self):
st, direction = MyTT.SUPERTREND(np.array([]), np.array([]), np.array([]))
assert len(st) == 0 and len(direction) == 0
class TestCHANDELIER:
def test_stops_bracket_price(self):
_, high, low, close, _ = _ohlcv()
long_stop, short_stop = MyTT.CHANDELIER(close, high, low, N=22, M=22, K=3.0)
# 吊灯止损锚定通道极值:多头止损在 N 日最高价下方、空头止损在 N 日最低价上方
# (下跌段中 long_stop 可以高于当根 high——这正是吊灯线滞后等待离场的行为)
valid = slice(22, None) # TR[0] 为 NaNREF 前收盘缺失)→ ATR 自 22 起有效
assert (long_stop[valid] < MyTT.HHV(high, 22)[valid]).all()
assert (short_stop[valid] > MyTT.LLV(low, 22)[valid]).all()
def test_matches_manual_formula(self):
_, high, low, close, _ = _ohlcv()
long_stop, _ = MyTT.CHANDELIER(close, high, low, N=22, M=22, K=2.0)
expected = MyTT.HHV(high, 22) - MyTT.ATR(close, high, low, 22) * 2.0
assert np.allclose(long_stop, expected, equal_nan=True)
class TestICHIMOKU:
def test_five_outputs_lengths(self):
_, high, low, close, _ = _ohlcv()
outs = MyTT.ICHIMOKU(high, low, close)
assert len(outs) == 5
for arr in outs:
assert len(arr) == len(close)
def test_tenkan_formula(self):
_, high, low, close, _ = _ohlcv()
tenkan, _, _, _, _ = MyTT.ICHIMOKU(high, low, close, P1=9, P2=26, P3=52)
expected = (MyTT.HHV(high, 9) + MyTT.LLV(low, 9)) / 2
assert np.allclose(tenkan, expected, equal_nan=True)
def test_span_is_shifted_past(self):
# 先行带 = 26 期前的 (转换线+基准线)/2:i 处的值来自 i-26(过去)
_, high, low, close, _ = _ohlcv()
_, _, span_a, _, _ = MyTT.ICHIMOKU(high, low, close, P1=9, P2=26, P3=52, SHIFT=26)
tenkan = (MyTT.HHV(high, 9) + MyTT.LLV(low, 9)) / 2
kijun = (MyTT.HHV(high, 26) + MyTT.LLV(low, 26)) / 2
raw = (tenkan + kijun) / 2
assert np.allclose(span_a[26:], raw[:-26], equal_nan=True)
def test_chikou_tail_nan(self):
# 迟行带 = 当前收盘画回 26 期前:末尾 26 个槽位无对应未来数据 → NaN
_, high, low, close, _ = _ohlcv()
*_, chikou = MyTT.ICHIMOKU(high, low, close)
assert np.isnan(chikou[-26:]).all()
assert not np.isnan(chikou[:-26]).any()
class TestUOS:
def test_range_zero_to_hundred(self):
_, high, low, close, _ = _ohlcv()
uos, uos_ma = MyTT.UOS(close, high, low)
valid = uos[28:] # bp[0] 为 NaNREF 前收盘缺失)→ P3=28 窗口自 28 起有效
assert (valid >= 0).all() and (valid <= 100).all()
def test_new_low_oversold(self):
# 持续创新低 → UOS 应处于超卖区(<50)
low = np.linspace(50, 1, 60)
close = low.copy()
high = low + 0.5
uos, _ = MyTT.UOS(close, high, low)
assert uos[-1] < 50
def test_flat_market_neutral(self):
flat = np.full(60, 10.0)
uos, _ = MyTT.UOS(flat, flat, flat)
assert np.allclose(uos[28:], 50.0, equal_nan=True) # 除零保护取中性
class TestCMO:
def test_symmetric_range(self):
close = _ohlcv()[3]
cmo = MyTT.CMO(close, N=14)
valid = cmo[14:]
assert (valid >= -100).all() and (valid <= 100).all()
def test_rising_positive_falling_negative(self):
rise = np.cumsum(np.ones(100))
assert MyTT.CMO(rise, N=14)[-1] > 0 # 纯上涨 → +100 极值
fall = 100 - np.cumsum(np.ones(100))
assert MyTT.CMO(fall, N=14)[-1] < 0 # 纯下跌 → -100 极值
class TestTSI:
def test_signal_line_follows(self):
close = _ohlcv()[3]
tsi, signal = MyTT.TSI(close)
assert len(tsi) == len(signal) == len(close)
valid = ~np.isnan(tsi) & ~np.isnan(signal)
assert valid.any()
def test_strong_rise_positive(self):
close = np.cumsum(np.ones(100))
tsi, _ = MyTT.TSI(close)
assert tsi[-1] > 0
class TestFISHER:
def test_trigger_is_prev_value(self):
_, high, low, _, _ = _ohlcv()
fisher, trigger = MyTT.FISHER(high, low, N=9)
assert np.allclose(trigger[1:], fisher[:-1], equal_nan=True)
def test_strong_rise_positive_sharply(self):
high = np.linspace(1, 50, 100)
low = high - 0.5
fisher, _ = MyTT.FISHER(high, low, N=9)
assert fisher[-1] > 1.0 # 顶部区域输出尖峰
assert np.isfinite(fisher[8:]).all() # 钳制保证无 inf
def test_bounded_input_clamp(self):
# 归一化值被钳制在 ±0.999 → 输出有限
_, high, low, _, _ = _ohlcv()
fisher, _ = MyTT.FISHER(high, low, N=3)
assert np.isfinite(fisher[2:]).all()
class TestSQUEEZE:
def test_bool_flag_and_mom_length(self):
_, high, low, close, _ = _ohlcv()
sqz, mom = MyTT.SQUEEZE(close, high, low)
assert sqz.dtype == bool
assert len(sqz) == len(mom) == len(close)
def test_flat_market_mom_zero(self):
flat = np.full(60, 10.0)
_, mom = MyTT.SQUEEZE(flat, flat, flat)
# 双层 N=20 窗口(带宽层 + 回归层)→ 有效值自 2N-1 起
assert np.allclose(mom[39:], 0.0, atol=1e-9)
class TestCHOP:
def test_range_and_warmup(self):
_, high, low, close, _ = _ohlcv()
chop = MyTT.CHOP(high, low, close, N=14)
assert np.isnan(chop[:14]).all() # TR[0] 为 NaN → 14 窗口自 14 起有效
valid = chop[14:]
assert (valid >= 0).all() and (valid <= 100).all()
def test_strong_trend_low_chop(self):
# 完美趋势:ΣTR ≈ 区间 → chop 趋近低值
high = np.arange(1, 81, dtype=float)
low = high - 1
close = high.copy()
chop = MyTT.CHOP(high, low, close, N=14)
assert chop[-1] < 30
def test_oscillation_high_chop(self):
# 剧烈往返震荡(窗口跨 2 个以上完整周期):路径远大于区间 → chop 高
t = np.arange(200)
close = 10 + 5 * np.sin(2 * np.pi * t / 6)
high = close + 0.5
low = close - 0.5
chop = MyTT.CHOP(high, low, close, N=14)
assert chop[14:].max() > 55
class TestADandCMF:
def test_ad_manual_clv(self):
# 单根:CLV=((C-L)-(H-C))/(H-L)AD=CLV×VOL 累计;C=11.5 → CLV=(1.5-0.5)/2=0.5
close = np.array([11.5, 11.5])
high = np.array([12.0, 12.0])
low = np.array([10.0, 10.0])
vol = np.array([100.0, 100.0])
ad = MyTT.AD(close, high, low, vol)
assert ad[0] == pytest.approx(0.5 * 100)
assert ad[1] == pytest.approx(100.0)
def test_cmf_range_and_warmup(self):
_, high, low, close, vol = _ohlcv()
cmf = MyTT.CMF(close, high, low, vol, N=20)
assert np.isnan(cmf[:19]).all()
valid = cmf[19:]
assert (valid >= -1).all() and (valid <= 1).all()
def test_cmf_sign_matches_close_position(self):
# 收盘持续靠近最高价(吸筹)→ CMF 为正
n = 60
close = np.linspace(10, 20, n)
high = close + 0.1
low = close - 1.0 # 收盘贴近最高
vol = np.full(n, 1000.0)
cmf = MyTT.CMF(close, high, low, vol, N=20)
assert cmf[-1] > 0
class TestEFI:
def test_rising_with_volume_positive(self):
n = 100
close = np.cumsum(np.ones(n))
vol = np.full(n, 1000.0)
efi = MyTT.EFI(close, vol, N=13)
assert efi[-1] > 0
def test_length_and_warmup(self):
close = _ohlcv()[3]
vol = _ohlcv()[4]
efi = MyTT.EFI(close, vol, N=13)
assert len(efi) == len(close)
assert np.isnan(efi[0]) # DIFF 首位 NaN
class TestBBPandBBW:
def test_bbp_position_semantics(self):
# N=3 手工窗口 [10, 14, x]mid=12、std=sqrt(8/3)close=mid → %B 恰为 50
c_mid = np.array([10.0, 14.0, 12.0])
assert MyTT.BBP(c_mid, N=3, P=2)[-1] == pytest.approx(50.0, abs=1e-6)
# 位置单调:同一窗口形态下,收盘越高 %B 越大
lo = MyTT.BBP(np.array([10.0, 14.0, 11.0]), N=3, P=2)[-1]
hi = MyTT.BBP(np.array([10.0, 14.0, 13.0]), N=3, P=2)[-1]
assert lo < 50.0 < hi
# 公式口径:直接用 numpy 独立重算 (C-(mid-2sd))/(4sd)*100RD 三位小数舍入)
window = np.array([10.0, 14.0, 13.0])
mid, sd = window.mean(), window.std()
expected = (13.0 - (mid - 2 * sd)) / (4 * sd) * 100
assert MyTT.BBP(window, N=3, P=2)[-1] == pytest.approx(expected, abs=1e-3)
def test_bbw_zero_when_flat(self):
bbw = MyTT.BBW(np.full(60, 10.0), N=20, P=2)
assert np.allclose(bbw[19:], 0.0)
def test_bbw_grows_with_volatility(self):
rng = np.random.default_rng(7)
quiet = 100 + rng.standard_normal(60) * 0.1
wild = 100 + rng.standard_normal(60) * 5.0
assert MyTT.BBW(wild)[-1] > MyTT.BBW(quiet)[-1]