mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 13:24:18 +08:00
release: v1.30.2 — 无未来函数指标扩容16个(注册指标50个)+ 内置策略补齐54个:WebUI回测全覆盖,新增前缀一致性无未来函数回归测试
This commit is contained in:
@@ -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.3,16 个函数)
|
||||
|
||||
- **趋势/止损类**:`SUPERTREND` 超级趋势(Wilder ATR 递推锁带,带线即移动止损,2025 年 TradingView 各"最佳指标"榜单常客)、`CHANDELIER` 吊灯止损(Chuck LeBeau,HHV−K×ATR 经典离场位)、`HMA` 赫尔均线(低滞后不重绘)、`KAMA` 考夫曼自适应均线(效率比驱动:趋势市贴价、震荡市自动走平)、`ICHIMOKU` 一目均衡表(五线一云;先行带用 26 期前的值画到当前位置,只引用过去数据——**迟行带 CHIKOU 按定义引用未来收盘,仅作图示对齐,当期信号严禁使用**,已在 docstring 与注册描述中显著标注)。
|
||||
- **动量/振荡类**:`UOS` 终极指标(Larry Williams,7/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% 确认转向后才会**回溯标出**,也就是说序列里每个拐点的位置都用到了"当时不可能知道"的未来信息。把它当买卖信号回测,等于允许策略在波谷那一天精准买入、在见顶前一天精准卖出——收益必然严重虚高、参数寻优必然过拟合,回测结果与实盘表现脱节,**这正是本工具最要防的失真**。
|
||||
|
||||
@@ -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。展示层设计对标专业终端(暗色、红涨绿跌、高信息密度),数据全部来自通达信协议直连——不花一分钱。
|
||||
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
@@ -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]]:
|
||||
"""返回所有可用指标的元数据。"""
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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] 为 NaN(REF 前收盘缺失)→ 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] 为 NaN(REF 前收盘缺失)→ 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)*100(RD 三位小数舍入)
|
||||
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]
|
||||
|
||||
Reference in New Issue
Block a user