99import pandas as pd
1010
1111
12- MINIMUM_FUNCTION_COUNT = 129
12+ MINIMUM_FUNCTION_COUNT = 201
1313
14+ TA_LIB_081_FUNCTIONS = frozenset ({
15+ "AC" , "ADR" , "AO" , "CMF" , "CMOU" , "COPPOCK" , "CUMSUM" , "CVI" ,
16+ "DONCHIAN" , "DPO" , "EFI" , "ER" , "ERI" , "FOSC" , "FRACTAL" , "HA" ,
17+ "HMA" , "KC" , "KDJ" , "MARKETFI" , "MASSI" , "NVI" , "PERCENTILE" ,
18+ "PERCENTRANK" , "PVI" , "PVO" , "PVT" , "QSTICK" , "RMA" , "RVI" , "RVOL" ,
19+ "SMI" , "SUPERTREND" , "TSI" , "VHF" , "VORTEX" , "VWAP" , "VWMA" , "WAD" ,
20+ "ZLEMA" ,
21+ })
1422
1523class TalibFactorError (ValueError ):
1624 def __init__ (self , code : str ):
@@ -41,32 +49,38 @@ def list_talib_factors() -> list[dict[str, Any]]:
4149 required_fields = tuple (sorted (_flatten_input_names (info .get ("input_names" ) or {})))
4250 parameters = dict (info .get ("parameters" ) or {})
4351 outputs = list (info .get ("output_names" ) or [])
52+ if len (outputs ) > 1 :
53+ parameters ["output" ] = outputs [0 ]
4454 output .append ({
4555 "factor_id" : f"talib:{ name .upper ()} " ,
4656 "library_id" : name .upper (),
4757 "version" : str (getattr (talib , "__version__" , "" )),
58+ "display_name" : str (info .get ("display_name" ) or name ),
4859 "name_i18n_key" : f"factor.talib.{ name .lower ()} .name" ,
4960 "description_i18n_key" : f"factor.talib.{ name .lower ()} .description" ,
5061 "category" : _category (group_by_name .get (name .upper (), "" )),
5162 "factor_type" : "technical" ,
5263 "provider" : "ta-lib" ,
64+ "introduced_in" : "0.8.1" if name .upper () in TA_LIB_081_FUNCTIONS else "" ,
5365 "required_fields" : list (required_fields ),
5466 "default_params" : parameters ,
55- "parameter_schema" : {
56- key : {"type" : _parameter_type (value ), "default" : value }
57- for key , value in parameters .items ()
58- },
67+ "parameter_schema" : _parameter_schema (parameters , outputs ),
5968 "outputs" : outputs ,
69+ "direction_hint" : "neutral" ,
6070 "supported_contexts" : ["cta" , "portfolio" ],
6171 "default_warmup_bars" : int (getattr (function , "lookback" , 0 ) or 0 ),
6272 })
6373 return output
6474
6575
6676def assert_talib_catalog_ready () -> int :
67- count = len (list_talib_factors ())
68- if count < MINIMUM_FUNCTION_COUNT :
69- raise TalibFactorError (f"factor.talibCatalogIncomplete:{ count } " )
77+ catalog = list_talib_factors ()
78+ count = len (catalog )
79+ available = {item ["library_id" ] for item in catalog }
80+ missing = sorted (TA_LIB_081_FUNCTIONS - available )
81+ if count < MINIMUM_FUNCTION_COUNT or missing :
82+ suffix = f":{ ',' .join (missing )} " if missing else ""
83+ raise TalibFactorError (f"factor.talibCatalogIncomplete:{ count } { suffix } " )
7084 return count
7185
7286
@@ -88,10 +102,7 @@ def compute_talib_indicator(
88102 raise TalibFactorError (f"factor.missingFields:{ ',' .join (missing )} " )
89103 clean = frame .copy ()
90104 clean .columns = [str (column ).strip ().lower () for column in clean .columns ]
91- normalized = dict (params or {})
92- if library_id == "SMA" and "period" in normalized :
93- period = normalized .pop ("period" )
94- normalized .setdefault ("timeperiod" , period )
105+ normalized = _normalized_parameters (function , params )
95106 try :
96107 result = function (clean , ** normalized )
97108 except Exception as exc :
@@ -126,9 +137,11 @@ def compute_talib_factor(
126137 * ,
127138 output : str = "" ,
128139) -> float :
129- result = compute_talib_indicator (name , frame , params )
140+ normalized = dict (params or {})
141+ selected_from_params = normalized .pop ("output" , "" )
142+ result = compute_talib_indicator (name , frame , normalized )
130143 if isinstance (result , pd .DataFrame ):
131- selected = str (output or "" ).strip ()
144+ selected = str (output or selected_from_params or "" ).strip ()
132145 if not selected :
133146 selected = str (result .columns [0 ])
134147 if selected not in result .columns :
@@ -177,6 +190,38 @@ def _parameter_type(value: Any) -> str:
177190 return "string"
178191
179192
193+ def _parameter_schema (parameters : Mapping [str , Any ], outputs : list [str ]) -> dict [str , dict [str , Any ]]:
194+ schema = {}
195+ for key , value in parameters .items ():
196+ if key == "output" :
197+ schema [key ] = {"type" : "enum" , "default" : value , "options" : list (outputs )}
198+ continue
199+ item = {"type" : _parameter_type (value ), "default" : value }
200+ if item ["type" ] == "integer" :
201+ item .update ({"minimum" : 1 , "maximum" : 5000 , "step" : 1 })
202+ elif item ["type" ] == "number" :
203+ item .update ({"step" : 0.1 })
204+ schema [key ] = item
205+ return schema
206+
207+
208+ def _normalized_parameters (function : Any , params : Mapping [str , Any ] | None ) -> dict [str , Any ]:
209+ supported = set ((function .info .get ("parameters" ) or {}).keys ())
210+ normalized = dict (params or {})
211+ normalized .pop ("output" , None )
212+ aliases = {
213+ "period" : "timeperiod" ,
214+ "fast_period" : "fastperiod" ,
215+ "slow_period" : "slowperiod" ,
216+ "signal_period" : "signalperiod" ,
217+ }
218+ for source , target in aliases .items ():
219+ if source in normalized and target in supported :
220+ value = normalized .pop (source )
221+ normalized .setdefault (target , value )
222+ return normalized
223+
224+
180225def _category (group : str ) -> str :
181226 raw = str (group or "" ).strip ().lower ()
182227 if "pattern" in raw :
0 commit comments