Skip to content

Commit 28bcada

Browse files
committed
Add liquidity indicators and stats calculation
1 parent 765ec1e commit 28bcada

10 files changed

Lines changed: 1781 additions & 3 deletions

README.md

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ pip install pyindicators
8585
* [Market Structure CHoCH/BOS](#market-structure-chochbos)
8686
* [Liquidity Sweeps](#liquidity-sweeps)
8787
* [Buyside & Sellside Liquidity](#buyside--sellside-liquidity)
88+
* [Pure Price Action Liquidity Sweeps](#pure-price-action-liquidity-sweeps)
89+
* [Liquidity Pools](#liquidity-pools)
90+
* [Liquidity Levels / Voids (VP)](#liquidity-levels--voids-vp)
8891
* [Pattern recognition](#pattern-recognition)
8992
* [Detect Peaks](#detect-peaks)
9093
* [Detect Bullish Divergence](#detect-bullish-divergence)
@@ -1826,6 +1829,240 @@ The function returns:
18261829

18271830
![BUYSIDE_SELLSIDE_LIQUIDITY](https://github.com/coding-kitties/PyIndicators/blob/main/static/images/indicators/buy_side_sell_side_liquidity.png)
18281831

1832+
#### Pure Price Action Liquidity Sweeps
1833+
1834+
Pure Price Action Liquidity Sweeps is a Smart Money Concept indicator that uses recursive fractal swing detection to identify significant pivot levels and detect liquidity sweep events.
1835+
1836+
Unlike simple swing-based approaches, this indicator employs a hierarchical pivot detection algorithm with configurable depth to find progressively more significant swing points. A liquidity sweep occurs when price wicks through a pivot level without closing beyond it—indicating institutional stop-hunting. Levels are automatically invalidated once price closes through them (mitigated).
1837+
1838+
Three detection granularities are available:
1839+
1840+
- **Short Term** (depth 1) – detects all basic swing pivots, yielding the most sweep signals.
1841+
- **Intermediate Term** (depth 2) – uses two levels of fractal filtering for moderately significant pivots.
1842+
- **Long Term** (depth 3) – three levels of recursion, producing only the most significant swing points and fewest sweeps.
1843+
1844+
```python
1845+
def pure_price_action_liquidity_sweeps(
1846+
data: Union[PdDataFrame, PlDataFrame],
1847+
term: str = "long",
1848+
high_column: str = "High",
1849+
low_column: str = "Low",
1850+
close_column: str = "Close",
1851+
max_level_age: int = 2000,
1852+
bullish_sweep_column: str = "ppa_sweep_bullish",
1853+
bearish_sweep_column: str = "ppa_sweep_bearish",
1854+
sweep_high_column: str = "ppa_sweep_high",
1855+
sweep_low_column: str = "ppa_sweep_low",
1856+
) -> Union[PdDataFrame, PlDataFrame]:
1857+
```
1858+
1859+
Example
1860+
1861+
```python
1862+
import pandas as pd
1863+
from pyindicators import (
1864+
pure_price_action_liquidity_sweeps,
1865+
pure_price_action_liquidity_sweep_signal,
1866+
get_pure_price_action_liquidity_sweep_stats
1867+
)
1868+
1869+
# Create sample OHLC data
1870+
df = pd.DataFrame({
1871+
'High': [...],
1872+
'Low': [...],
1873+
'Close': [...]
1874+
})
1875+
1876+
# Detect pure price action liquidity sweeps (long-term fractal depth)
1877+
df = pure_price_action_liquidity_sweeps(df, term="long")
1878+
print(df[['ppa_sweep_bullish', 'ppa_sweep_bearish', 'ppa_sweep_high', 'ppa_sweep_low']])
1879+
1880+
# Generate trading signals
1881+
# 1 = bullish sweep, -1 = bearish sweep, 0 = no sweep
1882+
df = pure_price_action_liquidity_sweep_signal(df)
1883+
bullish_sweeps = df[df['ppa_sweep_signal'] == 1]
1884+
1885+
# Get statistics
1886+
stats = get_pure_price_action_liquidity_sweep_stats(df)
1887+
print(f"Total bullish sweeps: {stats['total_bullish']}")
1888+
print(f"Total bearish sweeps: {stats['total_bearish']}")
1889+
```
1890+
1891+
The function returns:
1892+
- `ppa_sweep_bullish`: 1 when a bullish sweep is detected (sell-side liquidity grabbed below a pivot low)
1893+
- `ppa_sweep_bearish`: 1 when a bearish sweep is detected (buy-side liquidity grabbed above a pivot high)
1894+
- `ppa_sweep_high`: Price level of the swept swing high on bearish-sweep bars
1895+
- `ppa_sweep_low`: Price level of the swept swing low on bullish-sweep bars
1896+
1897+
**Trading Strategy:**
1898+
- Use the `term` parameter to match your trading timeframe (short for scalping, long for swing trading)
1899+
- Bullish sweeps at pivot lows suggest smart money accumulation—potential long entries
1900+
- Bearish sweeps at pivot highs suggest smart money distribution—potential short entries
1901+
- Higher-depth sweeps (long term) are rarer but more significant
1902+
1903+
![PURE_PRICE_ACTION_LIQUIDITY_SWEEPS](https://github.com/coding-kitties/PyIndicators/blob/main/static/images/indicators/pure_price_action_liquidity_sweeps.png)
1904+
1905+
#### Liquidity Pools
1906+
1907+
Liquidity Pools is a Smart Money Concept indicator that identifies zones where resting orders cluster, detected by tracking areas where price repeatedly bounces (wicks) from a level.
1908+
1909+
A **bullish pool** (support) forms when price wicks below a body-bottom level multiple times without closing below it. A **bearish pool** (resistance) forms when price wicks above a body-top level multiple times without closing above it. Zones are mitigated (invalidated) when price closes through them on two consecutive bars.
1910+
1911+
Key parameters:
1912+
1913+
- **Contact Count** – minimum wick bounces required to form a pool (default: 2). Higher = fewer, more reliable zones.
1914+
- **Gap Bars** – minimum bars between contacts to prevent double-counting (default: 5).
1915+
- **Confirmation Bars** – bars price must stay away before confirming the zone (default: 10).
1916+
1917+
```python
1918+
def liquidity_pools(
1919+
data: Union[PdDataFrame, PlDataFrame],
1920+
contact_count: int = 2,
1921+
gap_bars: int = 5,
1922+
confirmation_bars: int = 10,
1923+
high_column: str = "High",
1924+
low_column: str = "Low",
1925+
open_column: str = "Open",
1926+
close_column: str = "Close",
1927+
volume_column: Optional[str] = "Volume",
1928+
bull_pool_top_column: str = "liq_pool_bull_top",
1929+
bull_pool_bottom_column: str = "liq_pool_bull_bottom",
1930+
bear_pool_top_column: str = "liq_pool_bear_top",
1931+
bear_pool_bottom_column: str = "liq_pool_bear_bottom",
1932+
bull_pool_formed_column: str = "liq_pool_bull_formed",
1933+
bear_pool_formed_column: str = "liq_pool_bear_formed",
1934+
bull_pool_mitigated_column: str = "liq_pool_bull_mitigated",
1935+
bear_pool_mitigated_column: str = "liq_pool_bear_mitigated",
1936+
) -> Union[PdDataFrame, PlDataFrame]:
1937+
```
1938+
1939+
Example
1940+
1941+
```python
1942+
import pandas as pd
1943+
from pyindicators import (
1944+
liquidity_pools,
1945+
liquidity_pool_signal,
1946+
get_liquidity_pool_stats
1947+
)
1948+
1949+
# Create sample OHLC data
1950+
df = pd.DataFrame({
1951+
'Open': [...],
1952+
'High': [...],
1953+
'Low': [...],
1954+
'Close': [...]
1955+
})
1956+
1957+
# Detect liquidity pools
1958+
df = liquidity_pools(df, contact_count=2, gap_bars=5, confirmation_bars=10)
1959+
print(df[['liq_pool_bull_top', 'liq_pool_bull_bottom',
1960+
'liq_pool_bear_top', 'liq_pool_bear_bottom']])
1961+
1962+
# Generate trading signals
1963+
# 1 = bullish pool formed (support), -1 = bearish pool formed (resistance)
1964+
df = liquidity_pool_signal(df)
1965+
pool_events = df[df['liq_pool_signal'] != 0]
1966+
1967+
# Get statistics
1968+
stats = get_liquidity_pool_stats(df)
1969+
print(f"Bull pools formed: {stats['total_bull_formed']}")
1970+
print(f"Bear pools formed: {stats['total_bear_formed']}")
1971+
print(f"Total mitigated: {stats['total_mitigated']}")
1972+
```
1973+
1974+
The function returns:
1975+
- `liq_pool_bull_top` / `liq_pool_bull_bottom`: Boundaries of the most recent active bullish pool (NaN if none)
1976+
- `liq_pool_bear_top` / `liq_pool_bear_bottom`: Boundaries of the most recent active bearish pool (NaN if none)
1977+
- `liq_pool_bull_formed` / `liq_pool_bear_formed`: 1 when a new pool forms
1978+
- `liq_pool_bull_mitigated` / `liq_pool_bear_mitigated`: 1 when a pool is mitigated (broken)
1979+
1980+
**Trading Strategy:**
1981+
- Bullish pools are support zones where institutional buyers accumulate—look for long entries near the zone
1982+
- Bearish pools are resistance zones where institutional sellers distribute—look for short entries near the zone
1983+
- Mitigation signals a change in market structure; the zone is no longer valid
1984+
- Increase `contact_count` for higher-quality, more reliable zones
1985+
1986+
#### Liquidity Levels / Voids (VP)
1987+
1988+
Liquidity Levels / Voids is a Smart Money Concept indicator that uses volume-profile analysis between swing points to identify price levels where little volume was traded — these are *liquidity voids* that price tends to revisit.
1989+
1990+
Between each pair of detected swing points, the price range is divided into equally-spaced levels and a volume profile is built. Levels where the traded volume is below a configurable threshold (as a fraction of the maximum level's volume) are classified as liquidity voids — low-volume zones that act as price magnets.
1991+
1992+
Key parameters:
1993+
1994+
- **Detection Length** — lookback/look-ahead period for swing detection (default: 47).
1995+
- **Threshold** — volume fraction below which a level is a void (default: 0.21, i.e. 21%).
1996+
- **Sensitivity** — number of price levels per swing range (default: 27). Higher = thinner, more granular zones.
1997+
1998+
```python
1999+
def liquidity_levels_voids(
2000+
data: Union[PdDataFrame, PlDataFrame],
2001+
detection_length: int = 47,
2002+
threshold: float = 0.21,
2003+
sensitivity: int = 27,
2004+
high_column: str = "High",
2005+
low_column: str = "Low",
2006+
close_column: str = "Close",
2007+
volume_column: Optional[str] = "Volume",
2008+
void_formed_column: str = "liq_void_formed",
2009+
void_filled_column: str = "liq_void_filled",
2010+
void_count_column: str = "liq_void_count",
2011+
void_nearest_top_column: str = "liq_void_nearest_top",
2012+
void_nearest_bottom_column: str = "liq_void_nearest_bot",
2013+
void_above_count_column: str = "liq_void_above_count",
2014+
void_below_count_column: str = "liq_void_below_count",
2015+
) -> Union[PdDataFrame, PlDataFrame]:
2016+
```
2017+
2018+
Example
2019+
2020+
```python
2021+
import pandas as pd
2022+
from pyindicators import (
2023+
liquidity_levels_voids,
2024+
liquidity_levels_voids_signal,
2025+
get_liquidity_levels_voids_stats
2026+
)
2027+
2028+
# Create sample OHLCV data
2029+
df = pd.DataFrame({
2030+
'High': [...],
2031+
'Low': [...],
2032+
'Close': [...],
2033+
'Volume': [...]
2034+
})
2035+
2036+
# Detect liquidity voids (volume-profile based)
2037+
df = liquidity_levels_voids(df, detection_length=47, threshold=0.21, sensitivity=27)
2038+
print(df[['liq_void_count', 'liq_void_nearest_top', 'liq_void_nearest_bot']])
2039+
2040+
# Generate directional signal based on void proximity
2041+
# 1 = price below nearest void (bullish magnet), -1 = price above (bearish magnet)
2042+
df = liquidity_levels_voids_signal(df)
2043+
signals = df[df['liq_void_signal'] != 0]
2044+
2045+
# Get statistics
2046+
stats = get_liquidity_levels_voids_stats(df)
2047+
print(f"Formation events: {stats['total_formation_events']}")
2048+
print(f"Fill events: {stats['total_fill_events']}")
2049+
print(f"Active voids: {stats['active_voids_last_bar']}")
2050+
```
2051+
2052+
The function returns:
2053+
- `liq_void_formed`: 1 on bars where new liquidity voids are identified
2054+
- `liq_void_filled`: 1 on bars where a void is filled (price crosses through it)
2055+
- `liq_void_count`: Total number of active unfilled voids
2056+
- `liq_void_nearest_top` / `liq_void_nearest_bot`: Boundaries of the nearest unfilled void to the current close
2057+
- `liq_void_above_count` / `liq_void_below_count`: Unfilled voids above/below the current price
2058+
2059+
**Trading Strategy:**
2060+
- Liquidity voids act as magnets — price is drawn to fill low-volume areas
2061+
- When price is below a void, expect it to be pulled up (bullish bias)
2062+
- When price is above a void, expect it to be pulled down (bearish bias)
2063+
- Use `liq_void_count` to gauge overall market imbalance
2064+
- Decrease `detection_length` for more frequent void detection on shorter timeframes
2065+
18292066
### Pattern Recognition
18302067

18312068
#### Detect Peaks

pyindicators/__init__.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,13 @@
2727
volume_gated_trend_ribbon,
2828
liquidity_sweeps, liquidity_sweep_signal, get_liquidity_sweep_stats,
2929
buyside_sellside_liquidity, buyside_sellside_liquidity_signal,
30-
get_buyside_sellside_liquidity_stats
30+
get_buyside_sellside_liquidity_stats,
31+
pure_price_action_liquidity_sweeps,
32+
pure_price_action_liquidity_sweep_signal,
33+
get_pure_price_action_liquidity_sweep_stats,
34+
liquidity_pools, liquidity_pool_signal, get_liquidity_pool_stats,
35+
liquidity_levels_voids, liquidity_levels_voids_signal,
36+
get_liquidity_levels_voids_stats
3137
)
3238
from .exceptions import PyIndicatorException
3339
from .date_range import DateRange
@@ -124,5 +130,14 @@ def get_version():
124130
'get_liquidity_sweep_stats',
125131
'buyside_sellside_liquidity',
126132
'buyside_sellside_liquidity_signal',
127-
'get_buyside_sellside_liquidity_stats'
133+
'get_buyside_sellside_liquidity_stats',
134+
'pure_price_action_liquidity_sweeps',
135+
'pure_price_action_liquidity_sweep_signal',
136+
'get_pure_price_action_liquidity_sweep_stats',
137+
'liquidity_pools',
138+
'liquidity_pool_signal',
139+
'get_liquidity_pool_stats',
140+
'liquidity_levels_voids',
141+
'liquidity_levels_voids_signal',
142+
'get_liquidity_levels_voids_stats'
128143
]

pyindicators/indicators/__init__.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,18 @@
5555
buyside_sellside_liquidity, buyside_sellside_liquidity_signal,
5656
get_buyside_sellside_liquidity_stats
5757
)
58+
from .pure_price_action_liquidity_sweeps import (
59+
pure_price_action_liquidity_sweeps,
60+
pure_price_action_liquidity_sweep_signal,
61+
get_pure_price_action_liquidity_sweep_stats
62+
)
63+
from .liquidity_pools import (
64+
liquidity_pools, liquidity_pool_signal, get_liquidity_pool_stats
65+
)
66+
from .liquidity_levels_voids import (
67+
liquidity_levels_voids, liquidity_levels_voids_signal,
68+
get_liquidity_levels_voids_stats
69+
)
5870

5971
__all__ = [
6072
'sma',
@@ -133,5 +145,14 @@
133145
'get_liquidity_sweep_stats',
134146
'buyside_sellside_liquidity',
135147
'buyside_sellside_liquidity_signal',
136-
'get_buyside_sellside_liquidity_stats'
148+
'get_buyside_sellside_liquidity_stats',
149+
'pure_price_action_liquidity_sweeps',
150+
'pure_price_action_liquidity_sweep_signal',
151+
'get_pure_price_action_liquidity_sweep_stats',
152+
'liquidity_pools',
153+
'liquidity_pool_signal',
154+
'get_liquidity_pool_stats',
155+
'liquidity_levels_voids',
156+
'liquidity_levels_voids_signal',
157+
'get_liquidity_levels_voids_stats'
137158
]

0 commit comments

Comments
 (0)