mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 20:24:19 +08:00
feat: add technical indicator calculation (30 indicators via MyTT), bump to 1.4.0
Integrate MyTT library to provide 30 technical indicators (MACD, KDJ, RSI, BOLL, DMI, ATR, etc.) accessible via API and CLI with automatic EMA warm-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
280af9ecf5
commit
bcddf5a052
@@ -0,0 +1,308 @@
|
||||
# MyTT 麦语言-通达信-同花顺指标实现 https://github.com/mpquant/MyTT
|
||||
# MyTT高级函数验证版本: https://github.com/mpquant/MyTT/blob/main/MyTT_plus.py
|
||||
# Python2老版本pandas特别的MyTT: https://github.com/mpquant/MyTT/blob/main/MyTT_python2.py
|
||||
# V2.1 2021-6-6 新增 BARSLAST函数 SLOPE,FORCAST线性回归预测函数
|
||||
# V2.3 2021-6-13 新增 TRIX,DPO,BRAR,DMA,MTM,MASS,ROC,VR,ASI等指标
|
||||
# V2.4 2021-6-27 新增 EXPMA,OBV,MFI指标, 改进SMA核心函数(核心函数彻底无循环)
|
||||
# V2.7 2021-11-21 修正 SLOPE,BARSLAST,函数,新加FILTER,LONGCROSS, 感谢qzhjiang对SLOPE,SMA等函数的指正
|
||||
# V2.8 2021-11-23 修正 FORCAST,WMA函数,欢迎qzhjiang,stanene,bcq加入社群,一起来完善myTT库
|
||||
# V2.9 2021-11-29 新增 HHVBARS,LLVBARS,CONST, VALUEWHEN功能函数
|
||||
# V2.92 2021-11-30 新增 BARSSINCEN函数,现在可以 pip install MyTT 完成安装
|
||||
# V3.0 2021-12-04 改进 DMA函数支持序列,新增XS2 薛斯通道II指标
|
||||
# V3.1 2021-12-19 新增 TOPRANGE,LOWRANGE一级函数
|
||||
# V3.2 2023-04-04 新增 CR指标
|
||||
# V3.3 2023-11-09 新增 SIN,COS,TAN序列处理的三角函数
|
||||
|
||||
|
||||
#以下所有函数如无特别说明,输入参数S均为numpy序列或者列表list,N为整型int
|
||||
#应用层1级函数完美兼容通达信或同花顺,具体使用方法请参考通达信
|
||||
|
||||
import numpy as np; import pandas as pd
|
||||
|
||||
#------------------ 0级:核心工具函数 --------------------------------------------
|
||||
def RD(N,D=3): return np.round(N,D) #四舍五入取3位小数
|
||||
def RET(S,N=1): return np.array(S)[-N] #返回序列倒数第N个值,默认返回最后一个
|
||||
def ABS(S): return np.abs(S) #返回N的绝对值
|
||||
def LN(S): return np.log(S) #求底是e的自然对数,
|
||||
def POW(S,N): return np.power(S,N) #求S的N次方
|
||||
def SQRT(S): return np.sqrt(S) #求S的平方根
|
||||
def SIN(S): return np.sin(S) #求S的正弦值(弧度)
|
||||
def COS(S): return np.cos(S) #求S的余弦值(弧度)
|
||||
def TAN(S): return np.tan(S) #求S的正切值(弧度)
|
||||
def MAX(S1,S2): return np.maximum(S1,S2) #序列max
|
||||
def MIN(S1,S2): return np.minimum(S1,S2) #序列min
|
||||
def IF(S,A,B): return np.where(S,A,B) #序列布尔判断 return=A if S==True else B
|
||||
|
||||
|
||||
def REF(S, N=1): #对序列整体下移动N,返回序列(shift后会产生NAN)
|
||||
return pd.Series(S).shift(N).values
|
||||
|
||||
def DIFF(S, N=1): #前一个值减后一个值,前面会产生nan
|
||||
return pd.Series(S).diff(N).values #np.diff(S)直接删除nan,会少一行
|
||||
|
||||
def STD(S,N): #求序列的N日标准差,返回序列
|
||||
return pd.Series(S).rolling(N).std(ddof=0).values
|
||||
|
||||
def SUM(S, N): #对序列求N天累计和,返回序列 N=0对序列所有依次求和
|
||||
return pd.Series(S).rolling(N).sum().values if N>0 else pd.Series(S).cumsum().values
|
||||
|
||||
def CONST(S): #返回序列S最后的值组成常量序列
|
||||
return np.full(len(S),S[-1])
|
||||
|
||||
def HHV(S,N): #HHV(C, 5) 最近5天收盘最高价
|
||||
return pd.Series(S).rolling(N).max().values
|
||||
|
||||
def LLV(S,N): #LLV(C, 5) 最近5天收盘最低价
|
||||
return pd.Series(S).rolling(N).min().values
|
||||
|
||||
def HHVBARS(S,N): #求N周期内S最高值到当前周期数, 返回序列
|
||||
return pd.Series(S).rolling(N).apply(lambda x: np.argmax(x[::-1]),raw=True).values
|
||||
|
||||
def LLVBARS(S,N): #求N周期内S最低值到当前周期数, 返回序列
|
||||
return pd.Series(S).rolling(N).apply(lambda x: np.argmin(x[::-1]),raw=True).values
|
||||
|
||||
def MA(S,N): #求序列的N日简单移动平均值,返回序列
|
||||
return pd.Series(S).rolling(N).mean().values
|
||||
|
||||
def EMA(S,N): #指数移动平均,为了精度 S>4*N EMA至少需要120周期 alpha=2/(span+1)
|
||||
return pd.Series(S).ewm(span=N, adjust=False).mean().values
|
||||
|
||||
def SMA(S, N, M=1): #中国式的SMA,至少需要120周期才精确 (雪球180周期) alpha=1/(1+com)
|
||||
return pd.Series(S).ewm(alpha=M/N,adjust=False).mean().values #com=N-M/M
|
||||
|
||||
def WMA(S, N): #通达信S序列的N日加权移动平均 Yn = (1*X1+2*X2+3*X3+...+n*Xn)/(1+2+3+...+Xn)
|
||||
return pd.Series(S).rolling(N).apply(lambda x:x[::-1].cumsum().sum()*2/N/(N+1),raw=True).values
|
||||
|
||||
def DMA(S, A): #求S的动态移动平均,A作平滑因子,必须 0<A<1 (此为核心函数,非指标)
|
||||
if isinstance(A,(int,float)): return pd.Series(S).ewm(alpha=A,adjust=False).mean().values
|
||||
A=np.array(A); A[np.isnan(A)]=1.0; Y= np.zeros(len(S)); Y[0]=S[0]
|
||||
for i in range(1,len(S)): Y[i]=A[i]*S[i]+(1-A[i])*Y[i-1] #A支持序列 by jqz1226
|
||||
return Y
|
||||
|
||||
def AVEDEV(S, N): #平均绝对偏差 (序列与其平均值的绝对差的平均值)
|
||||
return pd.Series(S).rolling(N).apply(lambda x: (np.abs(x - x.mean())).mean()).values
|
||||
|
||||
def SLOPE(S, N): #返S序列N周期回线性回归斜率
|
||||
return pd.Series(S).rolling(N).apply(lambda x: np.polyfit(range(N),x,deg=1)[0],raw=True).values
|
||||
|
||||
def FORCAST(S, N): #返回S序列N周期回线性回归后的预测值, jqz1226改进成序列出
|
||||
return pd.Series(S).rolling(N).apply(lambda x:np.polyval(np.polyfit(range(N),x,deg=1),N-1),raw=True).values
|
||||
|
||||
def LAST(S, A, B): #从前A日到前B日一直满足S_BOOL条件, 要求A>B & A>0 & B>=0
|
||||
return np.array(pd.Series(S).rolling(A+1).apply(lambda x:np.all(x[::-1][B:]),raw=True),dtype=bool)
|
||||
|
||||
#------------------ 1级:应用层函数(通过0级核心函数实现)使用方法请参考通达信--------------------------------
|
||||
def COUNT(S, N): # COUNT(CLOSE>O, N): 最近N天满足S_BOO的天数 True的天数
|
||||
return SUM(S,N)
|
||||
|
||||
def EVERY(S, N): # EVERY(CLOSE>O, 5) 最近N天是否都是True
|
||||
return IF(SUM(S,N)==N,True,False)
|
||||
|
||||
def EXIST(S, N): # EXIST(CLOSE>3010, N=5) n日内是否存在一天大于3000点
|
||||
return IF(SUM(S,N)>0,True,False)
|
||||
|
||||
def FILTER(S, N): # FILTER函数,S满足条件后,将其后N周期内的数据置为0, FILTER(C==H,5)
|
||||
for i in range(len(S)): S[i+1:i+1+N]=0 if S[i] else S[i+1:i+1+N]
|
||||
return S # 例:FILTER(C==H,5) 涨停后,后5天不再发出信号
|
||||
|
||||
def BARSLAST(S): #上一次条件成立到当前的周期, BARSLAST(C/REF(C,1)>=1.1) 上一次涨停到今天的天数
|
||||
M=np.concatenate(([0],np.where(S,1,0)))
|
||||
for i in range(1, len(M)): M[i]=0 if M[i] else M[i-1]+1
|
||||
return M[1:]
|
||||
|
||||
def BARSLASTCOUNT(S): # 统计连续满足S条件的周期数 by jqz1226
|
||||
rt = np.zeros(len(S)+1) # BARSLASTCOUNT(CLOSE>OPEN)表示统计连续收阳的周期数
|
||||
for i in range(len(S)): rt[i+1]=rt[i]+1 if S[i] else rt[i+1]
|
||||
return rt[1:]
|
||||
|
||||
def BARSSINCEN(S, N): # N周期内第一次S条件成立到现在的周期数,N为常量 by jqz1226
|
||||
return pd.Series(S).rolling(N).apply(lambda x:N-1-np.argmax(x) if np.argmax(x) or x[0] else 0,raw=True).fillna(0).values.astype(int)
|
||||
|
||||
def CROSS(S1, S2): # 判断向上金叉穿越 CROSS(MA(C,5),MA(C,10)) 判断向下死叉穿越 CROSS(MA(C,10),MA(C,5))
|
||||
return np.concatenate(([False], np.logical_not((S1>S2)[:-1]) & (S1>S2)[1:])) # 不使用0级函数,移植方便 by jqz1226
|
||||
|
||||
def LONGCROSS(S1,S2,N): # 两条线维持一定周期后交叉,S1在N周期内都小于S2,本周期从S1下方向上穿过S2时返回1,否则返回0
|
||||
return np.array(np.logical_and(LAST(S1<S2,N,1),(S1>S2)),dtype=bool) # N=1时等同于CROSS(S1, S2)
|
||||
|
||||
def VALUEWHEN(S, X): # 当S条件成立时,取X的当前值,否则取VALUEWHEN的上个成立时的X值 by jqz1226
|
||||
return pd.Series(np.where(S,X,np.nan)).ffill().values
|
||||
|
||||
def BETWEEN(S, A, B): # S处于A和B之间时为真。 包括 A<S<B 或 A>S>B
|
||||
return ((A<S) & (S<B)) | ((A>S) & (S>B))
|
||||
|
||||
def TOPRANGE(S): # TOPRANGE(HIGH)表示当前最高价是近多少周期内最高价的最大值 by jqz1226
|
||||
rt = np.zeros(len(S))
|
||||
for i in range(1,len(S)): rt[i] = np.argmin(np.flipud(S[:i]<S[i]))
|
||||
return rt.astype('int')
|
||||
|
||||
def LOWRANGE(S): # LOWRANGE(LOW)表示当前最低价是近多少周期内最低价的最小值 by jqz1226
|
||||
rt = np.zeros(len(S))
|
||||
for i in range(1,len(S)): rt[i] = np.argmin(np.flipud(S[:i]>S[i]))
|
||||
return rt.astype('int')
|
||||
|
||||
|
||||
#------------------ 2级:技术指标函数(全部通过0级,1级函数实现) ------------------------------
|
||||
def MACD(CLOSE,SHORT=12,LONG=26,M=9): # EMA的关系,S取120日,和雪球小数点2位相同
|
||||
DIF = EMA(CLOSE,SHORT)-EMA(CLOSE,LONG);
|
||||
DEA = EMA(DIF,M); MACD=(DIF-DEA)*2
|
||||
return RD(DIF),RD(DEA),RD(MACD)
|
||||
|
||||
def KDJ(CLOSE,HIGH,LOW, N=9,M1=3,M2=3): # KDJ指标
|
||||
low_n = LLV(LOW, N)
|
||||
high_n = HHV(HIGH, N)
|
||||
high_low_diff = high_n - low_n
|
||||
# 避免除零:当最高价等于最低价时,RSV 应该为 50(中性)
|
||||
with np.errstate(divide='ignore', invalid='ignore'):
|
||||
rsv = (CLOSE - low_n) / high_low_diff * 100
|
||||
rsv = np.where(high_low_diff == 0, 50, rsv) # 除零时返回 50
|
||||
K = EMA(rsv, (M1*2-1)); D = EMA(K,(M2*2-1)); J=K*3-D*2
|
||||
return K, D, J
|
||||
|
||||
def RSI(CLOSE, N=24): # RSI指标,和通达信小数点2位相同
|
||||
DIF = CLOSE-REF(CLOSE,1)
|
||||
abs_dif_sma = SMA(ABS(DIF), N)
|
||||
# 避免除零:当价格完全不变时,RSI 应该为 50(中性)
|
||||
with np.errstate(divide='ignore', invalid='ignore'):
|
||||
rsi_value = SMA(MAX(DIF,0), N) / abs_dif_sma * 100
|
||||
rsi_value = np.where(abs_dif_sma == 0, 50, rsi_value) # 除零时返回 50
|
||||
return RD(rsi_value)
|
||||
|
||||
def WR(CLOSE, HIGH, LOW, N=10, N1=6): #W&R 威廉指标
|
||||
high_n = HHV(HIGH, N)
|
||||
low_n = LLV(LOW, N)
|
||||
high_low_diff = high_n - low_n
|
||||
with np.errstate(divide='ignore', invalid='ignore'):
|
||||
wr = (high_n - CLOSE) / high_low_diff * 100
|
||||
wr = np.where(high_low_diff == 0, 50, wr) # 除零时返回 50
|
||||
|
||||
high_n1 = HHV(HIGH, N1)
|
||||
low_n1 = LLV(LOW, N1)
|
||||
high_low_diff1 = high_n1 - low_n1
|
||||
with np.errstate(divide='ignore', invalid='ignore'):
|
||||
wr1 = (high_n1 - CLOSE) / high_low_diff1 * 100
|
||||
wr1 = np.where(high_low_diff1 == 0, 50, wr1) # 除零时返回 50
|
||||
|
||||
return RD(wr), RD(wr1)
|
||||
|
||||
def BIAS(CLOSE,L1=6, L2=12, L3=24): # BIAS乖离率
|
||||
BIAS1 = (CLOSE - MA(CLOSE, L1)) / MA(CLOSE, L1) * 100
|
||||
BIAS2 = (CLOSE - MA(CLOSE, L2)) / MA(CLOSE, L2) * 100
|
||||
BIAS3 = (CLOSE - MA(CLOSE, L3)) / MA(CLOSE, L3) * 100
|
||||
return RD(BIAS1), RD(BIAS2), RD(BIAS3)
|
||||
|
||||
def BOLL(CLOSE,N=20, P=2): #BOLL指标,布林带
|
||||
MID = MA(CLOSE, N);
|
||||
UPPER = MID + STD(CLOSE, N) * P
|
||||
LOWER = MID - STD(CLOSE, N) * P
|
||||
return RD(UPPER), RD(MID), RD(LOWER)
|
||||
|
||||
def PSY(CLOSE,N=12, M=6):
|
||||
PSY=COUNT(CLOSE>REF(CLOSE,1),N)/N*100
|
||||
PSYMA=MA(PSY,M)
|
||||
return RD(PSY),RD(PSYMA)
|
||||
|
||||
def CCI(CLOSE,HIGH,LOW,N=14):
|
||||
TP=(HIGH+LOW+CLOSE)/3
|
||||
return (TP-MA(TP,N))/(0.015*AVEDEV(TP,N))
|
||||
|
||||
def ATR(CLOSE,HIGH,LOW, N=20): #真实波动N日平均值
|
||||
TR = MAX(MAX((HIGH - LOW), ABS(REF(CLOSE, 1) - HIGH)), ABS(REF(CLOSE, 1) - LOW))
|
||||
return MA(TR, N)
|
||||
|
||||
def BBI(CLOSE,M1=3,M2=6,M3=12,M4=20): #BBI多空指标
|
||||
return (MA(CLOSE,M1)+MA(CLOSE,M2)+MA(CLOSE,M3)+MA(CLOSE,M4))/4
|
||||
|
||||
def DMI(CLOSE,HIGH,LOW,M1=14,M2=6): #动向指标:结果和同花顺,通达信完全一致
|
||||
TR = SUM(MAX(MAX(HIGH - LOW, ABS(HIGH - REF(CLOSE, 1))), ABS(LOW - REF(CLOSE, 1))), M1)
|
||||
HD = HIGH - REF(HIGH, 1); LD = REF(LOW, 1) - LOW
|
||||
DMP = SUM(IF((HD > 0) & (HD > LD), HD, 0), M1)
|
||||
DMM = SUM(IF((LD > 0) & (LD > HD), LD, 0), M1)
|
||||
PDI = DMP * 100 / TR; MDI = DMM * 100 / TR
|
||||
ADX = MA(ABS(MDI - PDI) / (PDI + MDI) * 100, M2)
|
||||
ADXR = (ADX + REF(ADX, M2)) / 2
|
||||
return PDI, MDI, ADX, ADXR
|
||||
|
||||
def TAQ(HIGH,LOW,N): #唐安奇通道(海龟)交易指标,大道至简,能穿越牛熊
|
||||
UP=HHV(HIGH,N); DOWN=LLV(LOW,N); MID=(UP+DOWN)/2
|
||||
return UP,MID,DOWN
|
||||
|
||||
def KTN(CLOSE,HIGH,LOW,N=20,M=10): #肯特纳交易通道, N选20日,ATR选10日
|
||||
MID=EMA((HIGH+LOW+CLOSE)/3,N)
|
||||
ATRN=ATR(CLOSE,HIGH,LOW,M)
|
||||
UPPER=MID+2*ATRN; LOWER=MID-2*ATRN
|
||||
return UPPER,MID,LOWER
|
||||
|
||||
def TRIX(CLOSE,M1=12, M2=20): #三重指数平滑平均线
|
||||
TR = EMA(EMA(EMA(CLOSE, M1), M1), M1)
|
||||
TRIX = (TR - REF(TR, 1)) / REF(TR, 1) * 100
|
||||
TRMA = MA(TRIX, M2)
|
||||
return TRIX, TRMA
|
||||
|
||||
def VR(CLOSE,VOL,M1=26): #VR容量比率
|
||||
LC = REF(CLOSE, 1)
|
||||
return SUM(IF(CLOSE > LC, VOL, 0), M1) / SUM(IF(CLOSE <= LC, VOL, 0), M1) * 100
|
||||
|
||||
def CR(CLOSE,HIGH,LOW,N=20): #CR价格动量指标
|
||||
MID=REF(HIGH+LOW+CLOSE,1)/3;
|
||||
return SUM(MAX(0,HIGH-MID),N)/SUM(MAX(0,MID-LOW),N)*100
|
||||
|
||||
def EMV(HIGH,LOW,VOL,N=14,M=9): #简易波动指标
|
||||
VOLUME=MA(VOL,N)/VOL; MID=100*(HIGH+LOW-REF(HIGH+LOW,1))/(HIGH+LOW)
|
||||
EMV=MA(MID*VOLUME*(HIGH-LOW)/MA(HIGH-LOW,N),N); MAEMV=MA(EMV,M)
|
||||
return EMV,MAEMV
|
||||
|
||||
|
||||
def DPO(CLOSE,M1=20, M2=10, M3=6): #区间震荡线
|
||||
DPO = CLOSE - REF(MA(CLOSE, M1), M2); MADPO = MA(DPO, M3)
|
||||
return DPO, MADPO
|
||||
|
||||
def BRAR(OPEN,CLOSE,HIGH,LOW,M1=26): #BRAR-ARBR 情绪指标
|
||||
AR = SUM(HIGH - OPEN, M1) / SUM(OPEN - LOW, M1) * 100
|
||||
BR = SUM(MAX(0, HIGH - REF(CLOSE, 1)), M1) / SUM(MAX(0, REF(CLOSE, 1) - LOW), M1) * 100
|
||||
return AR, BR
|
||||
|
||||
def DFMA(CLOSE,N1=10,N2=50,M=10): #平行线差指标
|
||||
DIF=MA(CLOSE,N1)-MA(CLOSE,N2); DIFMA=MA(DIF,M) #通达信指标叫DMA 同花顺叫新DMA
|
||||
return DIF,DIFMA
|
||||
|
||||
def MTM(CLOSE,N=12,M=6): #动量指标
|
||||
MTM=CLOSE-REF(CLOSE,N); MTMMA=MA(MTM,M)
|
||||
return MTM,MTMMA
|
||||
|
||||
def MASS(HIGH,LOW,N1=9,N2=25,M=6): #梅斯线
|
||||
MASS=SUM(MA(HIGH-LOW,N1)/MA(MA(HIGH-LOW,N1),N1),N2)
|
||||
MA_MASS=MA(MASS,M)
|
||||
return MASS,MA_MASS
|
||||
|
||||
def ROC(CLOSE,N=12,M=6): #变动率指标
|
||||
ROC=100*(CLOSE-REF(CLOSE,N))/REF(CLOSE,N); MAROC=MA(ROC,M)
|
||||
return ROC,MAROC
|
||||
|
||||
def EXPMA(CLOSE,N1=12,N2=50): #EMA指数平均数指标
|
||||
return EMA(CLOSE,N1),EMA(CLOSE,N2);
|
||||
|
||||
def OBV(CLOSE,VOL): #能量潮指标
|
||||
return SUM(IF(CLOSE>REF(CLOSE,1),VOL,IF(CLOSE<REF(CLOSE,1),-VOL,0)),0)/10000
|
||||
|
||||
def MFI(CLOSE,HIGH,LOW,VOL,N=14): #MFI指标是成交量的RSI指标
|
||||
TYP = (HIGH + LOW + CLOSE)/3
|
||||
V1=SUM(IF(TYP>REF(TYP,1),TYP*VOL,0),N)/SUM(IF(TYP<REF(TYP,1),TYP*VOL,0),N)
|
||||
return 100-(100/(1+V1))
|
||||
|
||||
def ASI(OPEN,CLOSE,HIGH,LOW,M1=26,M2=10): #振动升降指标
|
||||
LC=REF(CLOSE,1); AA=ABS(HIGH-LC); BB=ABS(LOW-LC);
|
||||
CC=ABS(HIGH-REF(LOW,1)); DD=ABS(LC-REF(OPEN,1));
|
||||
R=IF( (AA>BB) & (AA>CC),AA+BB/2+DD/4,IF( (BB>CC) & (BB>AA),BB+AA/2+DD/4,CC+DD/4));
|
||||
X=(CLOSE-LC+(CLOSE-OPEN)/2+LC-REF(OPEN,1));
|
||||
SI=16*X/R*MAX(AA,BB); ASI=SUM(SI,M1); ASIT=MA(ASI,M2);
|
||||
return ASI,ASIT
|
||||
|
||||
def XSII(CLOSE, HIGH, LOW, N=102, M=7): #薛斯通道II
|
||||
AA = MA((2*CLOSE + HIGH + LOW)/4, 5) #最新版DMA才支持 2021-12-4
|
||||
TD1 = AA*N/100; TD2 = AA*(200-N) / 100
|
||||
CC = ABS((2*CLOSE + HIGH + LOW)/4 - MA(CLOSE,20))/MA(CLOSE,20)
|
||||
DD = DMA(CLOSE,CC); TD3=(1+M/100)*DD; TD4=(1-M/100)*DD
|
||||
return TD1, TD2, TD3, TD4
|
||||
|
||||
|
||||
#望大家能提交更多指标和函数 https://github.com/mpquant/MyTT
|
||||
@@ -107,4 +107,4 @@ __all__ = [
|
||||
"save_best_ex_host",
|
||||
]
|
||||
|
||||
__version__ = "1.3.0"
|
||||
__version__ = "1.4.0"
|
||||
|
||||
@@ -11,6 +11,7 @@ from .cmd_capital import capital_flow
|
||||
from .cmd_ex import ex
|
||||
from .cmd_finance import f10, fund_flow
|
||||
from .cmd_info import server_info, symbol_info
|
||||
from .cmd_indicator import indicator, indicator_list
|
||||
from .cmd_kline import kline
|
||||
from .cmd_monitor import market_stat, unusual
|
||||
from .cmd_quote import quote, quote_list
|
||||
@@ -19,7 +20,7 @@ from .cmd_transaction import transaction
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.version_option(version="1.3.1", prog_name="easy-tdx")
|
||||
@click.version_option(version="1.4.0", prog_name="easy-tdx")
|
||||
def cli() -> None:
|
||||
"""easy-tdx -- 通达信行情数据 CLI(默认 JSON 输出,适合 Agent 使用)。
|
||||
|
||||
@@ -59,3 +60,5 @@ cli.add_command(symbol_info)
|
||||
cli.add_command(f10)
|
||||
cli.add_command(fund_flow)
|
||||
cli.add_command(ex)
|
||||
cli.add_command(indicator)
|
||||
cli.add_command(indicator_list)
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
"""技术指标命令。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import click
|
||||
|
||||
|
||||
def _parse_indicator_params(s: str) -> dict[str, dict[str, int | float]]:
|
||||
"""解析指标参数字符串。
|
||||
|
||||
格式: ``SHORT=10,LONG=22`` 或 ``MACD.SHORT=10,KDJ.N=14``
|
||||
无前缀的参数应用到所有请求的指标。
|
||||
"""
|
||||
result: dict[str, dict[str, int | float]] = {}
|
||||
if not s:
|
||||
return result
|
||||
|
||||
for pair in s.split(","):
|
||||
pair = pair.strip()
|
||||
if "=" not in pair:
|
||||
continue
|
||||
key, val = pair.split("=", 1)
|
||||
key = key.strip()
|
||||
val = val.strip()
|
||||
|
||||
if "." in key:
|
||||
indicator, param = key.split(".", 1)
|
||||
indicator = indicator.strip().upper()
|
||||
param = param.strip()
|
||||
result.setdefault(indicator, {})[param] = float(val) if "." in val else int(val)
|
||||
else:
|
||||
result.setdefault("*", {})[key] = float(val) if "." in val else int(val)
|
||||
return result
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.argument("indicators")
|
||||
@click.option("--market", "-m", required=True, help="市场: SH/SZ/BJ")
|
||||
@click.option("--code", "-c", required=True, help="股票代码")
|
||||
@click.option(
|
||||
"--period",
|
||||
default="DAILY",
|
||||
help="K线周期: DAILY/5MIN/15MIN/30MIN/60MIN/1MIN/WEEKLY/MONTHLY",
|
||||
)
|
||||
@click.option("--count", default=30, type=int, help="返回条数(默认30)")
|
||||
@click.option("--adjust", default="QFQ", help="复权: NONE/QFQ/HFQ(默认QFQ)")
|
||||
@click.option("--params", default=None, help="指标参数: SHORT=10,LONG=22 或 MACD.SHORT=10")
|
||||
@click.option("--no-ohlcv", is_flag=True, help="不显示原始OHLCV列")
|
||||
@click.option("--table", "use_table", is_flag=True, help="表格输出")
|
||||
@click.option("--output", "output_fmt", type=click.Choice(["json", "table", "csv"]), default="json")
|
||||
def indicator(
|
||||
indicators: str,
|
||||
market: str,
|
||||
code: str,
|
||||
period: str,
|
||||
count: int,
|
||||
adjust: str,
|
||||
params: str | None,
|
||||
no_ohlcv: bool,
|
||||
use_table: bool,
|
||||
output_fmt: str,
|
||||
) -> None:
|
||||
"""计算技术指标。
|
||||
|
||||
示例:
|
||||
|
||||
easy-tdx indicator MACD -m SH -c 600519 --table
|
||||
|
||||
easy-tdx indicator MACD,KDJ,RSI -m SH -c 600519 --count 10 --table
|
||||
|
||||
easy-tdx indicator BOLL -m SZ -c 000001 --params N=10,P=1.5
|
||||
"""
|
||||
from ..indicator import compute_indicators
|
||||
from .conn import get_mac_client
|
||||
from .output import print_error, print_output
|
||||
from .parsers import parse_adjust, parse_market, parse_period
|
||||
|
||||
fmt = "table" if use_table else output_fmt
|
||||
mkt = parse_market(market)
|
||||
indicator_list = [n.strip() for n in indicators.split(",")]
|
||||
parsed_params = _parse_indicator_params(params) if params else {}
|
||||
|
||||
# 将通配符参数应用到所有指标
|
||||
wildcard = parsed_params.pop("*", {})
|
||||
final_params: dict[str, dict[str, int | float]] = {}
|
||||
for name in indicator_list:
|
||||
final_params[name.upper()] = {**wildcard, **parsed_params.get(name.upper(), {})}
|
||||
|
||||
fetch_count = max(120 + count, 200)
|
||||
try:
|
||||
with get_mac_client() as client:
|
||||
df = client.get_stock_kline(
|
||||
mkt,
|
||||
code,
|
||||
period=parse_period(period),
|
||||
count=fetch_count,
|
||||
adjust=parse_adjust(adjust),
|
||||
)
|
||||
if df.empty:
|
||||
print_error("未获取到K线数据")
|
||||
return
|
||||
result = compute_indicators(
|
||||
df,
|
||||
indicator_list,
|
||||
final_params,
|
||||
keep_ohlcv=not no_ohlcv,
|
||||
tail=count,
|
||||
)
|
||||
print_output(result, fmt)
|
||||
except ValueError as e:
|
||||
print_error(str(e))
|
||||
except Exception as e:
|
||||
print_error(f"{type(e).__name__}: {e}")
|
||||
|
||||
|
||||
@click.command("indicator-list")
|
||||
@click.option("--table", "use_table", is_flag=True, help="表格输出")
|
||||
@click.option("--output", "output_fmt", type=click.Choice(["json", "table", "csv"]), default="json")
|
||||
def indicator_list(use_table: bool, output_fmt: str) -> None:
|
||||
"""列出可用的技术指标。"""
|
||||
import pandas as pd
|
||||
|
||||
from ..indicator import list_indicators
|
||||
from .output import print_output
|
||||
|
||||
fmt = "table" if use_table else output_fmt
|
||||
info = list_indicators()
|
||||
df = pd.DataFrame(info)
|
||||
if fmt == "table":
|
||||
df["default_params"] = df["default_params"].apply(lambda d: str(d))
|
||||
print_output(df, fmt)
|
||||
@@ -0,0 +1,277 @@
|
||||
"""技术指标计算模块 — 基于 MyTT 的纯计算层(无 IO)。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from . import MyTT
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class IndicatorSpec:
|
||||
"""单个技术指标的元数据。"""
|
||||
|
||||
name: str
|
||||
inputs: tuple[str, ...]
|
||||
outputs: tuple[str, ...]
|
||||
func: Callable[..., object]
|
||||
default_params: dict[str, int | float]
|
||||
description: str
|
||||
|
||||
|
||||
_REGISTRY: dict[str, IndicatorSpec] = {}
|
||||
|
||||
|
||||
def _reg(
|
||||
name: str,
|
||||
inputs: tuple[str, ...],
|
||||
outputs: tuple[str, ...],
|
||||
func: Callable[..., object],
|
||||
defaults: dict[str, int | float],
|
||||
desc: str,
|
||||
) -> None:
|
||||
_REGISTRY[name.upper()] = IndicatorSpec(
|
||||
name=name.upper(),
|
||||
inputs=inputs,
|
||||
outputs=outputs,
|
||||
func=func,
|
||||
default_params=defaults,
|
||||
description=desc,
|
||||
)
|
||||
|
||||
|
||||
# ── 仅需 close ──────────────────────────────────────────────────────────
|
||||
_reg(
|
||||
"MACD",
|
||||
("close",),
|
||||
("MACD_DIF", "MACD_DEA", "MACD_HIST"),
|
||||
MyTT.MACD,
|
||||
{"SHORT": 12, "LONG": 26, "M": 9},
|
||||
"MACD 指数平滑异同移动平均线",
|
||||
)
|
||||
_reg("RSI", ("close",), ("RSI",), MyTT.RSI, {"N": 24}, "RSI 相对强弱指标")
|
||||
_reg(
|
||||
"BOLL",
|
||||
("close",),
|
||||
("BOLL_UPPER", "BOLL_MID", "BOLL_LOWER"),
|
||||
MyTT.BOLL,
|
||||
{"N": 20, "P": 2},
|
||||
"BOLL 布林带",
|
||||
)
|
||||
_reg(
|
||||
"BIAS",
|
||||
("close",),
|
||||
("BIAS1", "BIAS2", "BIAS3"),
|
||||
MyTT.BIAS,
|
||||
{"L1": 6, "L2": 12, "L3": 24},
|
||||
"BIAS 乖离率",
|
||||
)
|
||||
_reg("PSY", ("close",), ("PSY", "PSY_MA"), MyTT.PSY, {"N": 12, "M": 6}, "PSY 心理线")
|
||||
_reg(
|
||||
"TRIX",
|
||||
("close",),
|
||||
("TRIX", "TRIX_MA"),
|
||||
MyTT.TRIX,
|
||||
{"M1": 12, "M2": 20},
|
||||
"TRIX 三重指数平滑平均线",
|
||||
)
|
||||
_reg(
|
||||
"DPO", ("close",), ("DPO", "DPO_MA"), MyTT.DPO, {"M1": 20, "M2": 10, "M3": 6}, "DPO 区间震荡线"
|
||||
)
|
||||
_reg("MTM", ("close",), ("MTM", "MTM_MA"), MyTT.MTM, {"N": 12, "M": 6}, "MTM 动量指标")
|
||||
_reg("ROC", ("close",), ("ROC", "ROC_MA"), MyTT.ROC, {"N": 12, "M": 6}, "ROC 变动率指标")
|
||||
_reg(
|
||||
"EXPMA",
|
||||
("close",),
|
||||
("EXPMA_12", "EXPMA_50"),
|
||||
MyTT.EXPMA,
|
||||
{"N1": 12, "N2": 50},
|
||||
"EXPMA 指数平均数指标",
|
||||
)
|
||||
_reg("BBI", ("close",), ("BBI",), MyTT.BBI, {"M1": 3, "M2": 6, "M3": 12, "M4": 20}, "BBI 多空指标")
|
||||
_reg(
|
||||
"DFMA",
|
||||
("close",),
|
||||
("DFMA_DIF", "DFMA_DMA"),
|
||||
MyTT.DFMA,
|
||||
{"N1": 10, "N2": 50, "M": 10},
|
||||
"DFMA 平行线差指标",
|
||||
)
|
||||
|
||||
# ── 需要 close + high + low ─────────────────────────────────────────────
|
||||
_reg(
|
||||
"KDJ",
|
||||
("close", "high", "low"),
|
||||
("KDJ_K", "KDJ_D", "KDJ_J"),
|
||||
MyTT.KDJ,
|
||||
{"N": 9, "M1": 3, "M2": 3},
|
||||
"KDJ 随机指标",
|
||||
)
|
||||
_reg(
|
||||
"DMI",
|
||||
("close", "high", "low"),
|
||||
("DMI_PDI", "DMI_MDI", "DMI_ADX", "DMI_ADXR"),
|
||||
MyTT.DMI,
|
||||
{"M1": 14, "M2": 6},
|
||||
"DMI 动向指标",
|
||||
)
|
||||
_reg("ATR", ("close", "high", "low"), ("ATR",), MyTT.ATR, {"N": 20}, "ATR 真实波幅均值")
|
||||
_reg("WR", ("close", "high", "low"), ("WR1", "WR2"), MyTT.WR, {"N": 10, "N1": 6}, "WR 威廉指标")
|
||||
_reg("CCI", ("close", "high", "low"), ("CCI",), MyTT.CCI, {"N": 14}, "CCI 顺势指标")
|
||||
_reg("CR", ("close", "high", "low"), ("CR",), MyTT.CR, {"N": 20}, "CR 价格动量指标")
|
||||
_reg(
|
||||
"KTN",
|
||||
("close", "high", "low"),
|
||||
("KTN_UPPER", "KTN_MID", "KTN_LOWER"),
|
||||
MyTT.KTN,
|
||||
{"N": 20, "M": 10},
|
||||
"KTN 肯特纳通道",
|
||||
)
|
||||
_reg(
|
||||
"XSII",
|
||||
("close", "high", "low"),
|
||||
("XSII_TD1", "XSII_TD2", "XSII_TD3", "XSII_TD4"),
|
||||
MyTT.XSII,
|
||||
{"N": 102, "M": 7},
|
||||
"XSII 薛斯通道II",
|
||||
)
|
||||
|
||||
# ── 需要 close + vol ────────────────────────────────────────────────────
|
||||
_reg("OBV", ("close", "vol"), ("OBV",), MyTT.OBV, {}, "OBV 能量潮指标")
|
||||
_reg("VR", ("close", "vol"), ("VR",), MyTT.VR, {"M1": 26}, "VR 容量比率")
|
||||
|
||||
# ── 需要 high + low + vol ───────────────────────────────────────────────
|
||||
_reg(
|
||||
"EMV",
|
||||
("high", "low", "vol"),
|
||||
("EMV", "EMV_MA"),
|
||||
MyTT.EMV,
|
||||
{"N": 14, "M": 9},
|
||||
"EMV 简易波动指标",
|
||||
)
|
||||
_reg(
|
||||
"MASS",
|
||||
("high", "low"),
|
||||
("MASS", "MASS_MA"),
|
||||
MyTT.MASS,
|
||||
{"N1": 9, "N2": 25, "M": 6},
|
||||
"MASS 梅斯线",
|
||||
)
|
||||
|
||||
# ── 需要 close + high + low + vol ──────────────────────────────────────
|
||||
_reg("MFI", ("close", "high", "low", "vol"), ("MFI",), MyTT.MFI, {"N": 14}, "MFI 资金流量指标")
|
||||
|
||||
# ── 需要 open + close + high + low ─────────────────────────────────────
|
||||
_reg("BRAR", ("open", "close", "high", "low"), ("AR", "BR"), MyTT.BRAR, {"M1": 26}, "BRAR 情绪指标")
|
||||
_reg(
|
||||
"ASI",
|
||||
("open", "close", "high", "low"),
|
||||
("ASI", "ASI_MA"),
|
||||
MyTT.ASI,
|
||||
{"M1": 26, "M2": 10},
|
||||
"ASI 振动升降指标",
|
||||
)
|
||||
|
||||
# ── 仅需 high + low ────────────────────────────────────────────────────
|
||||
_reg(
|
||||
"TAQ", ("high", "low"), ("TAQ_UP", "TAQ_MID", "TAQ_DOWN"), MyTT.TAQ, {"N": 20}, "TAQ 唐安奇通道"
|
||||
)
|
||||
|
||||
|
||||
def list_indicators() -> list[dict[str, object]]:
|
||||
"""返回所有可用指标的元数据。"""
|
||||
return [
|
||||
{
|
||||
"name": spec.name,
|
||||
"description": spec.description,
|
||||
"inputs": list(spec.inputs),
|
||||
"outputs": list(spec.outputs),
|
||||
"default_params": dict(spec.default_params),
|
||||
}
|
||||
for spec in _REGISTRY.values()
|
||||
]
|
||||
|
||||
|
||||
def compute_indicators(
|
||||
df: pd.DataFrame,
|
||||
indicators: list[str],
|
||||
params: dict[str, dict[str, int | float]] | None = None,
|
||||
keep_ohlcv: bool = True,
|
||||
tail: int | None = None,
|
||||
) -> pd.DataFrame:
|
||||
"""在 K 线 DataFrame 上计算指定技术指标。
|
||||
|
||||
Args:
|
||||
df: K 线数据,需包含 open/close/high/low/vol 等列。
|
||||
indicators: 指标名称列表(不区分大小写),如 ``["MACD", "KDJ"]``。
|
||||
params: 可选参数覆盖,如 ``{"MACD": {"SHORT": 10}}``。
|
||||
keep_ohlcv: True 则保留原始 OHLCV 列。
|
||||
tail: 计算后仅保留最后 N 行。
|
||||
|
||||
Returns:
|
||||
包含指标列的 DataFrame。
|
||||
"""
|
||||
if df.empty:
|
||||
return pd.DataFrame(df.copy())
|
||||
|
||||
params = params or {}
|
||||
result_parts: list[pd.DataFrame] = []
|
||||
required_inputs: set[str] = set()
|
||||
|
||||
names_upper = [n.strip().upper() for n in indicators]
|
||||
unknown = [n for n in names_upper if n not in _REGISTRY]
|
||||
if unknown:
|
||||
raise ValueError(f"未知指标: {unknown}。可用指标: {sorted(_REGISTRY.keys())}")
|
||||
|
||||
for name in names_upper:
|
||||
spec = _REGISTRY[name]
|
||||
required_inputs.update(spec.inputs)
|
||||
|
||||
missing_cols = required_inputs - set(df.columns)
|
||||
if missing_cols:
|
||||
raise ValueError(f"DataFrame 缺少必要列: {missing_cols}。指标需要这些列: {required_inputs}")
|
||||
|
||||
if len(df) < 120:
|
||||
warnings.warn(
|
||||
f"数据仅 {len(df)} 行,EMA 类指标至少需要 120 行才能精确收敛",
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
for name in names_upper:
|
||||
spec = _REGISTRY[name]
|
||||
inputs = tuple(df[col].values for col in spec.inputs)
|
||||
override = params.get(name, params.get(spec.name, {}))
|
||||
kwargs = {**spec.default_params, **override}
|
||||
raw = spec.func(*inputs, **kwargs)
|
||||
|
||||
if isinstance(raw, tuple):
|
||||
arrays = raw
|
||||
else:
|
||||
arrays = (raw,)
|
||||
|
||||
if len(arrays) != len(spec.outputs):
|
||||
raise RuntimeError(f"{name}: 预期 {len(spec.outputs)} 个输出,实际 {len(arrays)} 个")
|
||||
|
||||
part = pd.DataFrame(
|
||||
{col: arr for col, arr in zip(spec.outputs, arrays)},
|
||||
index=df.index,
|
||||
)
|
||||
result_parts.append(part)
|
||||
|
||||
indicator_df: pd.DataFrame = pd.concat(result_parts, axis=1)
|
||||
|
||||
if keep_ohlcv:
|
||||
out: pd.DataFrame = pd.concat([df, indicator_df], axis=1)
|
||||
else:
|
||||
time_cols = [c for c in ("datetime", "date") if c in df.columns]
|
||||
out = pd.concat([df[time_cols], indicator_df], axis=1) if time_cols else indicator_df
|
||||
|
||||
if tail is not None and tail > 0:
|
||||
out = out.iloc[-tail:]
|
||||
|
||||
return pd.DataFrame(out.reset_index(drop=True))
|
||||
@@ -375,6 +375,37 @@ class MacClient:
|
||||
|
||||
return _to_df(all_bars)
|
||||
|
||||
def get_stock_kline_with_indicators(
|
||||
self,
|
||||
market: int,
|
||||
code: str,
|
||||
indicators: list[str],
|
||||
period: Period = Period.DAILY,
|
||||
count: int = 30,
|
||||
adjust: Adjust = Adjust.QFQ,
|
||||
params: dict[str, dict[str, int | float]] | None = None,
|
||||
) -> pd.DataFrame:
|
||||
"""获取 K 线数据并计算技术指标。
|
||||
|
||||
自动获取足够的历史数据用于指标预热(EMA 至少需要 120 周期)。
|
||||
|
||||
Args:
|
||||
market: 市场代码。
|
||||
code: 股票代码。
|
||||
indicators: 指标名称列表,如 ``["MACD", "KDJ"]``。
|
||||
period: K 线周期。
|
||||
count: 返回条数(默认30)。
|
||||
adjust: 复权方式(默认前复权)。
|
||||
params: 可选指标参数覆盖。
|
||||
"""
|
||||
from ..indicator import compute_indicators
|
||||
|
||||
fetch_count = max(120 + count, 200)
|
||||
df = self.get_stock_kline(market, code, period=period, count=fetch_count, adjust=adjust)
|
||||
if df.empty:
|
||||
return df
|
||||
return compute_indicators(df, indicators, params, tail=count)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 分时
|
||||
# ------------------------------------------------------------------ #
|
||||
@@ -1140,6 +1171,30 @@ class AsyncMacClient:
|
||||
|
||||
return _to_df(all_bars)
|
||||
|
||||
async def get_stock_kline_with_indicators(
|
||||
self,
|
||||
market: int,
|
||||
code: str,
|
||||
indicators: list[str],
|
||||
period: Period = Period.DAILY,
|
||||
count: int = 30,
|
||||
adjust: Adjust = Adjust.QFQ,
|
||||
params: dict[str, dict[str, int | float]] | None = None,
|
||||
) -> pd.DataFrame:
|
||||
"""获取 K 线数据并计算技术指标(异步)。
|
||||
|
||||
自动获取足够的历史数据用于指标预热(EMA 至少需要 120 周期)。
|
||||
"""
|
||||
from ..indicator import compute_indicators
|
||||
|
||||
fetch_count = max(120 + count, 200)
|
||||
df = await self.get_stock_kline(
|
||||
market, code, period=period, count=fetch_count, adjust=adjust,
|
||||
)
|
||||
if df.empty:
|
||||
return df
|
||||
return compute_indicators(df, indicators, params, tail=count)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 分时
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@@ -125,6 +125,20 @@ class UnifiedTdxClient:
|
||||
) -> pd.DataFrame:
|
||||
return self._ensure_mac().get_stock_kline(market, code, period, start, count, times, adjust)
|
||||
|
||||
def get_stock_kline_with_indicators(
|
||||
self,
|
||||
market: int,
|
||||
code: str,
|
||||
indicators: list[str],
|
||||
period: Period = Period.DAILY,
|
||||
count: int = 30,
|
||||
adjust: Adjust = Adjust.QFQ,
|
||||
params: dict[str, dict[str, int | float]] | None = None,
|
||||
) -> pd.DataFrame:
|
||||
return self._ensure_mac().get_stock_kline_with_indicators(
|
||||
market, code, indicators, period, count, adjust, params,
|
||||
)
|
||||
|
||||
def get_tick_chart(
|
||||
self,
|
||||
market: int,
|
||||
@@ -397,6 +411,21 @@ class AsyncUnifiedTdxClient:
|
||||
mac = await self._ensure_mac()
|
||||
return await mac.get_stock_kline(market, code, period, start, count, times, adjust)
|
||||
|
||||
async def get_stock_kline_with_indicators(
|
||||
self,
|
||||
market: int,
|
||||
code: str,
|
||||
indicators: list[str],
|
||||
period: Period = Period.DAILY,
|
||||
count: int = 30,
|
||||
adjust: Adjust = Adjust.QFQ,
|
||||
params: dict[str, dict[str, int | float]] | None = None,
|
||||
) -> pd.DataFrame:
|
||||
mac = await self._ensure_mac()
|
||||
return await mac.get_stock_kline_with_indicators(
|
||||
market, code, indicators, period, count, adjust, params,
|
||||
)
|
||||
|
||||
async def get_tick_chart(
|
||||
self,
|
||||
market: int,
|
||||
|
||||
Reference in New Issue
Block a user