@@ -327,6 +327,302 @@ def internal_external_liquidity_zones_signal(
327327 )
328328
329329
330+ def internal_external_liquidity_zones_signal_live (
331+ data : Union [PdDataFrame , PlDataFrame ],
332+ internal_pivot_length : int = 3 ,
333+ external_pivot_length : int = 10 ,
334+ atr_length : int = 14 ,
335+ zone_size_atr : float = 0.40 ,
336+ high_column : str = "High" ,
337+ low_column : str = "Low" ,
338+ ext_high_column : str = "ielz_ext_high" ,
339+ ext_low_column : str = "ielz_ext_low" ,
340+ ext_high_price_column : str = "ielz_ext_high_price" ,
341+ ext_low_price_column : str = "ielz_ext_low_price" ,
342+ int_high_column : str = "ielz_int_high" ,
343+ int_low_column : str = "ielz_int_low" ,
344+ int_high_price_column : str = "ielz_int_high_price" ,
345+ int_low_price_column : str = "ielz_int_low_price" ,
346+ ext_sweep_bull_column : str = "ielz_live_ext_sweep_bull" ,
347+ ext_sweep_bear_column : str = "ielz_live_ext_sweep_bear" ,
348+ int_sweep_bull_column : str = "ielz_live_int_sweep_bull" ,
349+ int_sweep_bear_column : str = "ielz_live_int_sweep_bear" ,
350+ signal_column : str = "ielz_live_signal" ,
351+ ) -> Union [PdDataFrame , PlDataFrame ]:
352+ """
353+ Generate trading signals from IELZ zones **without lookahead bias**.
354+
355+ Unlike :func:`internal_external_liquidity_zones_signal`, this
356+ function delays zone activation by the pivot confirmation window.
357+ Pivots are only confirmed ``pivot_length`` bars after the pivot
358+ bar, so zones cannot trigger sweeps until they are confirmed.
359+
360+ This makes the signals suitable for **live trading** and
361+ **realistic backtesting**.
362+
363+ Args:
364+ data: DataFrame with IELZ columns (output of
365+ :func:`internal_external_liquidity_zones`).
366+ internal_pivot_length: The internal pivot length used in the
367+ original calculation (must match). Zones are delayed by
368+ this many bars.
369+ external_pivot_length: The external pivot length used in the
370+ original calculation (must match). External zones are
371+ delayed by this many bars.
372+ atr_length: ATR period used for zone sizing
373+ (default: 14).
374+ zone_size_atr: Half-height of zones as ATR fraction
375+ (default: 0.40). Must match original calculation.
376+ high_column: Column name for highs.
377+ low_column: Column name for lows.
378+ ext_high_column: Column with external high zone flags.
379+ ext_low_column: Column with external low zone flags.
380+ ext_high_price_column: Column with external high prices.
381+ ext_low_price_column: Column with external low prices.
382+ int_high_column: Column with internal high zone flags.
383+ int_low_column: Column with internal low zone flags.
384+ int_high_price_column: Column with internal high prices.
385+ int_low_price_column: Column with internal low prices.
386+ ext_sweep_bull_column: Output column for live external
387+ bullish sweeps.
388+ ext_sweep_bear_column: Output column for live external
389+ bearish sweeps.
390+ int_sweep_bull_column: Output column for live internal
391+ bullish sweeps.
392+ int_sweep_bear_column: Output column for live internal
393+ bearish sweeps.
394+ signal_column: Output column for combined signal.
395+
396+ Returns:
397+ DataFrame with live sweep columns and combined signal:
398+
399+ - ``{ext_sweep_bull_column}`` - 1 on external low zone sweep
400+ - ``{ext_sweep_bear_column}`` - 1 on external high zone sweep
401+ - ``{int_sweep_bull_column}`` - 1 on internal low zone sweep
402+ - ``{int_sweep_bear_column}`` - 1 on internal high zone sweep
403+ - ``{signal_column}`` - 1 (bullish), -1 (bearish), or 0
404+
405+ Example:
406+ >>> df = internal_external_liquidity_zones(df, ...)
407+ >>> df = internal_external_liquidity_zones_signal_live(
408+ ... df,
409+ ... internal_pivot_length=3,
410+ ... external_pivot_length=10,
411+ ... )
412+ >>> # Use ielz_live_signal for trading
413+ """
414+ if isinstance (data , PlDataFrame ):
415+ import polars as pl
416+
417+ pd_data = data .to_pandas ()
418+ result = _ielz_signal_live_pandas (
419+ pd_data ,
420+ internal_pivot_length = internal_pivot_length ,
421+ external_pivot_length = external_pivot_length ,
422+ atr_length = atr_length ,
423+ zone_size_atr = zone_size_atr ,
424+ high_column = high_column ,
425+ low_column = low_column ,
426+ ext_high_column = ext_high_column ,
427+ ext_low_column = ext_low_column ,
428+ ext_high_price_column = ext_high_price_column ,
429+ ext_low_price_column = ext_low_price_column ,
430+ int_high_column = int_high_column ,
431+ int_low_column = int_low_column ,
432+ int_high_price_column = int_high_price_column ,
433+ int_low_price_column = int_low_price_column ,
434+ ext_sweep_bull_column = ext_sweep_bull_column ,
435+ ext_sweep_bear_column = ext_sweep_bear_column ,
436+ int_sweep_bull_column = int_sweep_bull_column ,
437+ int_sweep_bear_column = int_sweep_bear_column ,
438+ signal_column = signal_column ,
439+ )
440+ return pl .from_pandas (result )
441+ elif isinstance (data , PdDataFrame ):
442+ return _ielz_signal_live_pandas (
443+ data ,
444+ internal_pivot_length = internal_pivot_length ,
445+ external_pivot_length = external_pivot_length ,
446+ atr_length = atr_length ,
447+ zone_size_atr = zone_size_atr ,
448+ high_column = high_column ,
449+ low_column = low_column ,
450+ ext_high_column = ext_high_column ,
451+ ext_low_column = ext_low_column ,
452+ ext_high_price_column = ext_high_price_column ,
453+ ext_low_price_column = ext_low_price_column ,
454+ int_high_column = int_high_column ,
455+ int_low_column = int_low_column ,
456+ int_high_price_column = int_high_price_column ,
457+ int_low_price_column = int_low_price_column ,
458+ ext_sweep_bull_column = ext_sweep_bull_column ,
459+ ext_sweep_bear_column = ext_sweep_bear_column ,
460+ int_sweep_bull_column = int_sweep_bull_column ,
461+ int_sweep_bear_column = int_sweep_bear_column ,
462+ signal_column = signal_column ,
463+ )
464+ else :
465+ raise PyIndicatorException (
466+ "Input data must be a pandas or polars DataFrame."
467+ )
468+
469+
470+ def _ielz_signal_live_pandas (
471+ data : PdDataFrame ,
472+ * ,
473+ internal_pivot_length : int ,
474+ external_pivot_length : int ,
475+ atr_length : int ,
476+ zone_size_atr : float ,
477+ high_column : str ,
478+ low_column : str ,
479+ ext_high_column : str ,
480+ ext_low_column : str ,
481+ ext_high_price_column : str ,
482+ ext_low_price_column : str ,
483+ int_high_column : str ,
484+ int_low_column : str ,
485+ int_high_price_column : str ,
486+ int_low_price_column : str ,
487+ ext_sweep_bull_column : str ,
488+ ext_sweep_bear_column : str ,
489+ int_sweep_bull_column : str ,
490+ int_sweep_bear_column : str ,
491+ signal_column : str ,
492+ ) -> PdDataFrame :
493+ """Core pandas implementation for live signal generation."""
494+ data = data .copy ()
495+ n = len (data )
496+
497+ high_arr = data [high_column ].values .astype (float )
498+ low_arr = data [low_column ].values .astype (float )
499+
500+ # Compute ATR for zone sizing
501+ close_arr = data ["Close" ].values .astype (float )
502+ atr_arr = _compute_atr (high_arr , low_arr , close_arr , atr_length )
503+
504+ # Zone detection columns (these have lookahead in their detection)
505+ ext_high_flags = data [ext_high_column ].values
506+ ext_low_flags = data [ext_low_column ].values
507+ ext_high_prices = data [ext_high_price_column ].values
508+ ext_low_prices = data [ext_low_price_column ].values
509+
510+ int_high_flags = data [int_high_column ].values
511+ int_low_flags = data [int_low_column ].values
512+ int_high_prices = data [int_high_price_column ].values
513+ int_low_prices = data [int_low_price_column ].values
514+
515+ # Output arrays
516+ out_ext_sweep_bull = np .zeros (n , dtype = int )
517+ out_ext_sweep_bear = np .zeros (n , dtype = int )
518+ out_int_sweep_bull = np .zeros (n , dtype = int )
519+ out_int_sweep_bear = np .zeros (n , dtype = int )
520+
521+ # Pending zones: detected but not yet confirmed
522+ pending_zones : List [dict ] = []
523+ # Active zones: confirmed and ready for sweep detection
524+ active_zones : List [dict ] = []
525+
526+ for i in range (n ):
527+ cur_atr = atr_arr [i ] if not np .isnan (atr_arr [i ]) else 0.0
528+ half_zone = cur_atr * zone_size_atr * 0.5
529+
530+ # ── Detect new zones and add to pending ──────────────────
531+ zone_defs = [
532+ (ext_high_flags , ext_high_prices ,
533+ external_pivot_length , True , True ),
534+ (ext_low_flags , ext_low_prices ,
535+ external_pivot_length , False , True ),
536+ (int_high_flags , int_high_prices ,
537+ internal_pivot_length , True , False ),
538+ (int_low_flags , int_low_prices ,
539+ internal_pivot_length , False , False ),
540+ ]
541+
542+ for flags , prices , delay , is_high , is_external in zone_defs :
543+ if flags [i ] == 1 :
544+ price = prices [i ]
545+ if not np .isnan (price ):
546+ # Zone detected at bar i, confirmed at bar i + delay
547+ pending_zones .append ({
548+ "detected" : i ,
549+ "confirmed" : i + delay ,
550+ "price" : price ,
551+ "top" : price + half_zone ,
552+ "bottom" : price - half_zone ,
553+ "is_high" : is_high ,
554+ "is_external" : is_external ,
555+ "state" : 0 , # 0 = active, 1 = swept
556+ })
557+
558+ # ── Move confirmed zones to active ───────────────────────
559+ newly_confirmed = [z for z in pending_zones if z ["confirmed" ] <= i ]
560+ active_zones .extend (newly_confirmed )
561+ pending_zones = [z for z in pending_zones if z ["confirmed" ] > i ]
562+
563+ # ── Check for sweeps on active zones ─────────────────────
564+ h_i = high_arr [i ]
565+ lo_i = low_arr [i ]
566+
567+ for zone in active_zones :
568+ if zone ["state" ] != 0 :
569+ continue
570+
571+ swept = False
572+ if zone ["is_high" ]:
573+ # High zone: swept when price wicks into zone from below
574+ if h_i >= zone ["bottom" ]:
575+ swept = True
576+ else :
577+ # Low zone: swept when price wicks into zone from above
578+ if lo_i <= zone ["top" ]:
579+ swept = True
580+
581+ if swept :
582+ zone ["state" ] = 1
583+ if zone ["is_external" ]:
584+ if zone ["is_high" ]:
585+ out_ext_sweep_bear [i ] = 1
586+ else :
587+ out_ext_sweep_bull [i ] = 1
588+ else :
589+ if zone ["is_high" ]:
590+ out_int_sweep_bear [i ] = 1
591+ else :
592+ out_int_sweep_bull [i ] = 1
593+
594+ # ── Remove swept zones to keep list manageable ───────────
595+ active_zones = [z for z in active_zones if z ["state" ] == 0 ]
596+
597+ # ── Build combined signal ────────────────────────────────────
598+ signal = np .where (
599+ out_ext_sweep_bull == 1 ,
600+ 1 ,
601+ np .where (
602+ out_ext_sweep_bear == 1 ,
603+ - 1 ,
604+ np .where (
605+ out_int_sweep_bull == 1 ,
606+ 1 ,
607+ np .where (
608+ out_int_sweep_bear == 1 ,
609+ - 1 ,
610+ 0 ,
611+ ),
612+ ),
613+ ),
614+ )
615+
616+ # ── Write output columns ─────────────────────────────────────
617+ data [ext_sweep_bull_column ] = out_ext_sweep_bull
618+ data [ext_sweep_bear_column ] = out_ext_sweep_bear
619+ data [int_sweep_bull_column ] = out_int_sweep_bull
620+ data [int_sweep_bear_column ] = out_int_sweep_bear
621+ data [signal_column ] = signal
622+
623+ return data
624+
625+
330626def get_internal_external_liquidity_zones_stats (
331627 data : Union [PdDataFrame , PlDataFrame ],
332628 ext_high_column : str = "ielz_ext_high" ,
0 commit comments