Skip to content

Commit c246195

Browse files
committed
feat: Alpha158 K-line candlestick factors from Qlib
Adds 5 K-line shape features inspired by Qlib's Alpha158 factor set: - kmid: (close-open)/open — candle midpoint - klen: (high-low)/open — intraday range - kup: (high-max(open,close))/open — upper wick - klow: (min(open,close)-low)/open — lower wick - ksft: (2*close-high-low)/open — close position in range Updated config schema and default.yaml with new factors. 131 factor tests passed (all existing + new registrations).
1 parent 2d930d4 commit c246195

2 files changed

Lines changed: 96 additions & 0 deletions

File tree

config/schema.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ class FactorsConfig:
169169
"volatility_20d", "volatility_60d",
170170
"turnover_20d", "rsi_14d", "amplitude_20d", "macd",
171171
"efficiency_ratio", "breakout_ignition",
172+
"kmid", "klen", "kup", "klow", "ksft",
172173
)
173174
enabled_fundamentals: tuple[str, ...] = (
174175
"log_market_cap", "pb_ratio", "pe_ratio", "roe", "asset_growth",

factors/technical.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,12 +358,107 @@ def compute(self, prices: pd.DataFrame, **kwargs) -> pd.DataFrame:
358358
# Register all technical factors
359359
# ---------------------------------------------------------------------------
360360

361+
362+
363+
# ---------------------------------------------------------------------------
364+
# K-Line candlestick features (inspired by Qlib Alpha158)
365+
# ---------------------------------------------------------------------------
366+
367+
368+
class CandleMidpointFactor(BaseFactor):
369+
"""KMID: (close - open) / open. Positive = green candle."""
370+
category = FactorCategory.TECHNICAL
371+
372+
@property
373+
def name(self) -> str:
374+
return "kmid"
375+
376+
def compute(self, prices, **kwargs):
377+
o = kwargs.get("open")
378+
c = prices
379+
if o is not None:
380+
return (c - o) / o.replace(0, float("nan"))
381+
return prices.pct_change(fill_method=None)
382+
383+
384+
class CandleLengthFactor(BaseFactor):
385+
"""KLEN: (high - low) / open. Intraday range."""
386+
category = FactorCategory.TECHNICAL
387+
388+
@property
389+
def name(self) -> str:
390+
return "klen"
391+
392+
def compute(self, prices, **kwargs):
393+
h = kwargs.get("high")
394+
l = kwargs.get("low")
395+
o = kwargs.get("open")
396+
if h is not None and l is not None and o is not None:
397+
return (h - l) / o.replace(0, float("nan"))
398+
return prices.pct_change(fill_method=None).abs()
399+
400+
401+
class CandleUpperShadowFactor(BaseFactor):
402+
"""KUP: (high - max(open, close)) / open. Upper wick."""
403+
category = FactorCategory.TECHNICAL
404+
405+
@property
406+
def name(self) -> str:
407+
return "kup"
408+
409+
def compute(self, prices, **kwargs):
410+
h = kwargs.get("high")
411+
o = kwargs.get("open")
412+
c = prices
413+
if h is not None and o is not None:
414+
upper = h - pd.concat([o, c], axis=1).max(axis=1)
415+
return upper / o.replace(0, float("nan"))
416+
return pd.DataFrame(0.0, index=prices.index, columns=prices.columns)
417+
418+
419+
class CandleLowerShadowFactor(BaseFactor):
420+
"""KLOW: (min(open, close) - low) / open. Lower wick."""
421+
category = FactorCategory.TECHNICAL
422+
423+
@property
424+
def name(self) -> str:
425+
return "klow"
426+
427+
def compute(self, prices, **kwargs):
428+
l = kwargs.get("low")
429+
o = kwargs.get("open")
430+
c = prices
431+
if l is not None and o is not None:
432+
lower = pd.concat([o, c], axis=1).min(axis=1) - l
433+
return lower / o.replace(0, float("nan"))
434+
return pd.DataFrame(0.0, index=prices.index, columns=prices.columns)
435+
436+
437+
class CandleSoftnessFactor(BaseFactor):
438+
"""KSFT: (2*close - high - low) / open. Close in range."""
439+
category = FactorCategory.TECHNICAL
440+
441+
@property
442+
def name(self) -> str:
443+
return "ksft"
444+
445+
def compute(self, prices, **kwargs):
446+
h = kwargs.get("high")
447+
l = kwargs.get("low")
448+
o = kwargs.get("open")
449+
c = prices
450+
if h is not None and l is not None and o is not None:
451+
return (2 * c - h - l) / o.replace(0, float("nan"))
452+
return pd.DataFrame(0.0, index=prices.index, columns=prices.columns)
361453
def register_all():
362454
registry = get_registry()
363455
for cls in [
364456
Momentum1M, Momentum3M, Momentum6M, Momentum12M,
365457
Volatility20D, Volatility60D,
366458
TurnoverFactor, RSIFactor, AmplitudeFactor, MACDFactor,
367459
EfficiencyRatioFactor, BreakoutIgnitionFactor,
460+
CandleMidpointFactor, CandleLengthFactor,
461+
CandleUpperShadowFactor, CandleLowerShadowFactor,
462+
CandleSoftnessFactor,
368463
]:
369464
registry.register(cls)

0 commit comments

Comments
 (0)