@@ -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 )
361453def 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