mirror of
https://github.com/OpenByteInc/QuantDinger.git
synced 2026-09-28 23:32:55 +08:00
128 lines
4.5 KiB
Python
128 lines
4.5 KiB
Python
"""
|
|
Technical indicator math aligned with mainstream CN terminals (同花顺 / 东方财富).
|
|
|
|
- KDJ(9,3,3): RSV on rolling HH/LL; K and D seeded at 50, then smoothed with 1/3 weight.
|
|
- RSI: Wilder smoothing (first average = SMA of first N changes).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import List, Optional, Sequence, Tuple
|
|
|
|
import numpy as np
|
|
|
|
|
|
def compute_kdj_cn(
|
|
high: Sequence[float],
|
|
low: Sequence[float],
|
|
close: Sequence[float],
|
|
period: int = 9,
|
|
k_smooth: int = 3,
|
|
d_smooth: int = 3,
|
|
) -> Tuple[List[Optional[float]], List[Optional[float]], List[Optional[float]]]:
|
|
"""KDJ with K/D initial value 50 (A-share terminal convention)."""
|
|
n = len(close)
|
|
k_out: List[Optional[float]] = [None] * n
|
|
d_out: List[Optional[float]] = [None] * n
|
|
j_out: List[Optional[float]] = [None] * n
|
|
if n < period or period < 1:
|
|
return k_out, d_out, j_out
|
|
|
|
k_prev = 50.0
|
|
d_prev = 50.0
|
|
for i in range(period - 1, n):
|
|
window_high = max(float(high[j]) for j in range(i - period + 1, i + 1))
|
|
window_low = min(float(low[j]) for j in range(i - period + 1, i + 1))
|
|
if window_high == window_low:
|
|
rsv = 50.0
|
|
else:
|
|
rsv = (float(close[i]) - window_low) / (window_high - window_low) * 100.0
|
|
|
|
k_prev = (k_prev * (k_smooth - 1) + rsv) / k_smooth
|
|
d_prev = (d_prev * (d_smooth - 1) + k_prev) / d_smooth
|
|
j_val = 3.0 * k_prev - 2.0 * d_prev
|
|
|
|
k_out[i] = round(k_prev, 4)
|
|
d_out[i] = round(d_prev, 4)
|
|
j_out[i] = round(j_val, 4)
|
|
|
|
return k_out, d_out, j_out
|
|
|
|
|
|
def compute_rsi_wilder(closes: Sequence[float], period: int = 14) -> List[Optional[float]]:
|
|
"""Wilder RSI; first valid value at index ``period``."""
|
|
n = len(closes)
|
|
out: List[Optional[float]] = [None] * n
|
|
if n < period + 1 or period < 1:
|
|
return out
|
|
|
|
gains: List[float] = []
|
|
losses: List[float] = []
|
|
for i in range(1, n):
|
|
chg = float(closes[i]) - float(closes[i - 1])
|
|
gains.append(chg if chg > 0 else 0.0)
|
|
losses.append(-chg if chg < 0 else 0.0)
|
|
|
|
avg_gain = sum(gains[:period]) / period
|
|
avg_loss = sum(losses[:period]) / period
|
|
out[period] = _rsi_from_avgs(avg_gain, avg_loss)
|
|
|
|
for i in range(period, len(gains)):
|
|
avg_gain = (avg_gain * (period - 1) + gains[i]) / period
|
|
avg_loss = (avg_loss * (period - 1) + losses[i]) / period
|
|
out[i + 1] = _rsi_from_avgs(avg_gain, avg_loss)
|
|
|
|
return out
|
|
|
|
|
|
def _rsi_from_avgs(avg_gain: float, avg_loss: float) -> float:
|
|
if avg_loss == 0:
|
|
return 100.0
|
|
rs = avg_gain / avg_loss
|
|
return round(100.0 - (100.0 / (1.0 + rs)), 4)
|
|
|
|
|
|
def kdj_codegen(period: int, k_smooth: int, d_smooth: int, col_prefix: str) -> str:
|
|
"""Pandas code fragment for strategy compiler (CN KDJ)."""
|
|
return f"""
|
|
# KDJ ({period},{k_smooth},{d_smooth}) — K/D seed 50 (CN terminal style)
|
|
_low_min = df['low'].rolling(window={period}).min()
|
|
_high_max = df['high'].rolling(window={period}).max()
|
|
_hl = (_high_max - _low_min).replace(0, np.nan)
|
|
_rsv = ((df['close'] - _low_min) / _hl * 100).fillna(50.0)
|
|
_k_list, _d_list = [], []
|
|
_k_prev, _d_prev = 50.0, 50.0
|
|
for _rv in _rsv:
|
|
if pd.isna(_rv):
|
|
_k_list.append(np.nan)
|
|
_d_list.append(np.nan)
|
|
continue
|
|
_k_prev = (_k_prev * ({k_smooth} - 1) + float(_rv)) / {k_smooth}
|
|
_d_prev = (_d_prev * ({d_smooth} - 1) + _k_prev) / {d_smooth}
|
|
_k_list.append(_k_prev)
|
|
_d_list.append(_d_prev)
|
|
df['{col_prefix}_k'] = pd.Series(_k_list, index=df.index)
|
|
df['{col_prefix}_d'] = pd.Series(_d_list, index=df.index)
|
|
df['{col_prefix}_j'] = 3 * df['{col_prefix}_k'] - 2 * df['{col_prefix}_d']
|
|
"""
|
|
|
|
|
|
def rsi_wilder_codegen(period: int, col_name: str) -> str:
|
|
"""Pandas code fragment for Wilder RSI (matches chart / market_data_collector)."""
|
|
return f"""
|
|
# RSI ({period}) — Wilder smoothing
|
|
_cl = df['close'].astype(float).tolist()
|
|
_rsi_out = [np.nan] * len(_cl)
|
|
if len(_cl) > {period}:
|
|
_g = [max(_cl[_i] - _cl[_i - 1], 0.0) for _i in range(1, len(_cl))]
|
|
_l = [max(_cl[_i - 1] - _cl[_i], 0.0) for _i in range(1, len(_cl))]
|
|
_ag = sum(_g[:{period}]) / {period}
|
|
_al = sum(_l[:{period}]) / {period}
|
|
_rsi_out[{period}] = 100.0 if _al == 0 else 100 - (100 / (1 + _ag / _al))
|
|
for _ii in range({period}, len(_g)):
|
|
_ag = (_ag * ({period} - 1) + _g[_ii]) / {period}
|
|
_al = (_al * ({period} - 1) + _l[_ii]) / {period}
|
|
_rsi_out[_ii + 1] = 100.0 if _al == 0 else 100 - (100 / (1 + _ag / _al))
|
|
df['{col_name}'] = pd.Series(_rsi_out, index=df.index)
|
|
"""
|