
You could be wasting hundreds on car insurance
You could be wasting hundreds every year on overpriced insurance. The experts at FinanceBuzz believe they can help. If your rate went up in the last 12 months, check out this new tool from FinanceBuzz to see if you’re overpaying in just a few clicks! They match drivers with companies reporting savings of $600 or more per year when switching!* Plus, once you use it, you’ll always have access to the lowest rates; best yet, it’s free. Answer a few easy questions to see how much you could be saving.
Elite Quant Plan – 14-Day Free Trial (This Week Only)
No card needed. Cancel anytime. Zero risk.
You get immediate access to:
Full code from every article (including today’s HMM notebook)
Private GitHub repos & templates
All premium deep dives (3–5 per month)
2 × 1-on-1 calls with me
One custom bot built/fixed for you
Try the entire Elite experience for 14 days — completely free.
→ Start your free trial now 👇
(Doors close in 7 days or when the post goes out of the spotlight — whichever comes first.)
See you on the inside.
👉 Upgrade Now →
🔔 Limited-Time Holiday Deal: 20% Off Our Complete 2026 Playbook! 🔔
Level up before the year ends!
AlgoEdge Insights: 30+ Python-Powered Trading Strategies – The Complete 2026 Playbook
30+ battle-tested algorithmic trading strategies from the AlgoEdge Insights newsletter – fully coded in Python, backtested, and ready to deploy. Your full arsenal for dominating 2026 markets.
Special Promo: Use code WINTER2025 for 20% off
Valid only until January 1, 2025 — act fast!
👇 Buy Now & Save 👇
Instant access to every strategy we've shared, plus exclusive extras.
— AlgoEdge Insights Team
Premium Members – Your Full Notebook Is Ready
The complete Google Colab notebook from today’s article (with live data, full Hidden Markov Model, interactive charts, statistics, and one-click CSV export) is waiting for you.
Preview of what you’ll get:

Inside:
Custom Volume-by-Price Profile Chart for any ticker (default: PLTR)
Automatic data download via yfinance (2024–2025 daily bars)
Splits volume into bullish (up candles) and bearish (down candles)
Builds mirrored horizontal histograms showing volume traded at each price level
Left panel: Bullish buying pressure (teal/green)
Right panel: Bearish selling pressure (red)
Center panel: Clean closing price line chart
Key levels marked: Peak volume, VWAP, and median price for bulls/bears
Bottom bars: Total bullish vs. bearish volume share (%)
Sleek dark theme with professional layout and translucent fills
Fully customizable: Change ticker, visible bars, tick size, or date range in seconds
Free readers – you already got the full breakdown and visuals in the article. Paid members – you get the actual tool.
Not upgraded yet? Fix that in 10 seconds here👇
Google Collab Notebook With Full Code Is Available In the End Of The Article Behind The Paywall 👇 (For Paid Subs Only)
Wederive an analysis to show where price conviction concentrates. We segment price into bullish/bearish buckets and aggregate volume directionally.
The aim is to see where market participants, both bears and bulls, commit capital and show dominance in key price zones.
This approach helps in spotting support & resistance. We borrow the visualization from liquidity dynamics, i.e. where transactions occur.
The complete Python notebook for the analysis is provided below.

1. The Dual-Sided Price-Volume Model
Traditional volume bars show how much was traded, but not where or in which direction.
We structure a price-volume map that splits volume into two directional layers:
Bullish Volume: Trades where the closing price exceeds the opening price (Ct > Ot)
Bearish Volume: Trades where the closing price falls below the opening price (Ct < Ot)
Each trading day contributes to one of these groups. We then aggregate volume by price bucket, rather than by time.
The time-series becomes a price-distribution model to show the capital concetrated across the price ladder.
If You Could Be Earlier Than 85% of the Market?
Most read the move after it runs. The top 250K start before the bell.
Elite Trade Club turns noise into a five-minute plan—what’s moving, why it matters, and the stocks to watch now. Miss it and you chase.
Catch it and you decide.
By joining, you’ll receive Elite Trade Club emails and select partner insights. See Privacy Policy.
Price Bucketing
To create consistent volume profiles, we round each closing price to a fixed tick size δ:

Pt is the closing price on day t, and δ is the tick size (e.g. $0.01). Each bucket holds aggregated volume from days that closed near that price.
Volume Aggregation
Once bucketed, we compute:

Ct: Close price
Ot: Open price
Vt: Volume
B: Set of all visible bars
p: Price bucket
Cumulative Volume Distribution
To quantify dominance, we normalize each side:

This cumulative fraction lets us interpret how deep into the price range buyers or sellers are committing.
A steeper curve means volume is front-loaded around a narrow zone, i.e. it shows conviction.
2. Implementation in Python
2.1. Set Parameters and Download Data
We start by importing libraries and defining user parameters.
These include the ticker symbol, date range, time interval, and number of bars to display.
The tick size controls the resolution of price bucketing.
We then use yfinance to fetch adjusted historical OHLCV data and retain the most recent VISIBLE_BARS.
import numpy as np
import pandas as pd
import yfinance as yf
import matplotlib.pyplot as plt
from matplotlib.dates import AutoDateLocator, AutoDateFormatter
# ── USER PARAMS
TICKER = "PLTR"
START = "2024-01-01"
END = "2025-07-13"
INTERVAL = "1d"
VISIBLE_BARS = 180 # decrease for fewer datapoints or increase for broader history
TICK_SIZE = 0.01 # smaller values give finer resolution, larger smooth the chart
# color definitions
BULL_EDGE_COLOR = "#089981"
BULL_FILL_COLOR = (8/255,153/255,129/255,0.90)
BEAR_EDGE_COLOR = "#F23645"
BEAR_FILL_COLOR = (242/255,54/255,69/255,0.90)
# ── FETCH DATA
df = yf.download(
TICKER,
start=START,
end=END,
interval=INTERVAL,
auto_adjust=True,
progress=False
)
if isinstance(df.columns, pd.MultiIndex):
df.columns = df.columns.get_level_values(0)
vis = df.tail(VISIBLE_BARS).copy()2.2. Segment Directional Trades and Bucket Prices
We label each row as either bullish or bearish based on the candle body. A day is bullish if it closes above the open, bearish if it closes below.
We then round each closing price to the nearest tick size to create discrete price buckets.
These buckets define the y-axis for our chart. For each side, bullish and bearish, we aggregate total volume by price bucket.
vis['bucket'] = (vis['Close']/TICK_SIZE).round().astype(int) * TICK_SIZE
bull = vis[vis['Close'] > vis['Open']]
bear = vis[vis['Close'] < vis['Open']]
bull_map = bull.groupby('bucket')['Volume'].sum()
bear_map = bear.groupby('bucket')['Volume'].sum()2.3. Compute Cumulative Distributions and Metrics
We normalize raw volume to prepare it for visual scaling. This avoids distortion from outlier bars.
Next, we calculate the total volume on each side and identify the price with the highest directional volume.
We sort price levels in descending order, then compute cumulative volume shares. These show how volume stacks up across the price ladder.
From the cumulative distribution, we extract the median price where volume share crosses 50 percent.
We also compute VWAP as a volume-weighted average across buckets. We draw these reference lines to mark where conviction is most concentrated.
Lastly, we define the price axis range using the min and max of all directional levels and raw price data.
# ── BUILD PRICE→VOLUME MAPS
vis['bucket'] = (vis['Close']/TICK_SIZE).round().astype(int) * TICK_SIZE
# split into directional candles
bull = vis[vis['Close'] > vis['Open']]
bear = vis[vis['Close'] < vis['Open']]
# group volume by price bucket
bull_map = bull.groupby('bucket')['Volume'].sum()
bear_map = bear.groupby('bucket')['Volume'].sum()
# scale factors for relative bar widths
bull_max = bull_map.max() if not bull_map.empty else 0
bear_max = bear_map.max() if not bear_map.empty else 0
# totals and peaks
bull_total = bull_map.sum()
bear_total = bear_map.sum()
bull_peak_price = bull_map.idxmax() if not bull_map.empty else np.nan
bear_peak_price = bear_map.idxmax() if not bear_map.empty else np.nan
# sort buckets top down
bull_levels = bull_map.sort_index(ascending=False)
bear_levels = bear_map.sort_index(ascending=False)
# cumulative volume fraction
bull_frac = bull_levels.cumsum() / bull_total if bull_total > 0 else pd.Series([], dtype=float)
bear_frac = bear_levels.cumsum() / bear_total if bear_total > 0 else pd.Series([], dtype=float)
# volume-weighted average price and median
bull_vwap = (bull_map.index.to_numpy() * bull_map.values).sum() / bull_total if bull_total > 0 else np.nan
bear_vwap = (bear_map.index.to_numpy() * bear_map.values).sum() / bear_total if bear_total > 0 else np.nan
bull_median = bull_frac[bull_frac >= 0.5].index[0] if not bull_frac.empty else np.nan
bear_median = bear_frac[bear_frac >= 0.5].index[0] if not bear_frac.empty else np.nan
# y-axis range
price_min = min(
bull_levels.index.min() if not bull_levels.empty else np.inf,
bear_levels.index.min() if not bear_levels.empty else np.inf,
vis['Low'].min()
)
price_max = max(
bull_levels.index.max() if not bull_levels.empty else -np.inf,
bear_levels.index.max() if not bear_levels.empty else -np.inf,
vis['High'].max()
)2.4. Build and Style the Chart Layout
We initialize a figure with three main panels: bullish volume on the left, close prices in the center, and bearish volume on the right.
Each side is rendered using add_axes for full control over the layout.
The bullish and bearish panels display horizontal bars scaled by relative volume, overlaid with a cumulative fill. Dotted, dashed, and dashdot lines mark the peak, VWAP, and median levels.
At the bottom, two summary boxes show the share of total volume contributed by bulls and bears. These provide a quick view of directional dominance.
# ── PLOT
plt.style.use('dark_background')
fig = plt.figure(figsize=(16,8)) # adjust to scale figure size
# layout positioning
left_margin = 0.05
bull_width = 0.20
price_width = 0.50
price_left = left_margin + bull_width
bear_left = price_left + price_width
bottom = 0.15
height = 0.75
box_height = 0.05
box_bottom = bottom - box_height
# price panel
price_ax = fig.add_axes([price_left, bottom, price_width, height])
price_ax.plot(vis.index, vis['Close'], color='white', lw=1)
price_ax.set_ylim(price_min, price_max)
date_loc = AutoDateLocator(minticks=5, maxticks=10)
price_ax.xaxis.set_major_locator(date_loc)
price_ax.xaxis.set_major_formatter(AutoDateFormatter(date_loc))
price_ax.tick_params(axis='x', rotation=45, colors='white', labelsize=8)
price_ax.tick_params(axis='y', colors='white', labelsize=8)
price_ax.set_title(f"{TICKER} – last {VISIBLE_BARS} bars", color='white')
# bullish depth panel (left)
bull_ax = fig.add_axes([left_margin, bottom, bull_width, height], sharey=price_ax)
if not bull_frac.empty:
if bull_max > 0:
bull_norm = bull_levels.values / bull_max
bull_ax.barh(
bull_levels.index,
bull_norm,
height=TICK_SIZE*3,
left=1 - bull_norm,
color='gray',
alpha=0.5
)
bull_ax.fill_betweenx(
bull_frac.index,
1 - bull_frac,
1,
facecolor=BULL_FILL_COLOR,
edgecolor=BULL_EDGE_COLOR,
linewidth=1, alpha=0.5
)
bull_ax.axhline(bull_peak_price, xmin=0, xmax=1, color=BULL_EDGE_COLOR, linestyle='dotted')
bull_ax.axhline(bull_median, xmin=0, xmax=1, color='gray', linestyle='dashdot')
bull_ax.axhline(bull_vwap, xmin=0, xmax=1, color='gray', linestyle='dashed')
bull_ax.set_xlim(0,1)
bull_ax.axis('off')
# bearish depth panel (right)
bear_ax = fig.add_axes([bear_left, bottom, bull_width, height], sharey=price_ax)
if not bear_frac.empty:
if bear_max > 0:
bear_norm = bear_levels.values / bear_max
bear_ax.barh(
bear_levels.index,
bear_norm,
height=TICK_SIZE*3,
left=0,
color='gray',
alpha=0.5
)
bear_ax.fill_betweenx(
bear_frac.index,
0,
bear_frac,
facecolor=BEAR_FILL_COLOR,
edgecolor=BEAR_EDGE_COLOR,
linewidth=1, alpha=0.5
)
bear_ax.axhline(bear_peak_price, xmin=0, xmax=1, color=BEAR_EDGE_COLOR, linestyle='dotted')
bear_ax.axhline(bear_median, xmin=0, xmax=1, color='gray', linestyle='dashdot')
bear_ax.axhline(bear_vwap, xmin=0, xmax=1, color='gray', linestyle='dashed')
bear_ax.set_xlim(0,1)
bear_ax.axis('off')
# volume share summary
bull_share = bull_total / (bull_total + bear_total)
bear_share = bear_total / (bull_total + bear_total)
bull_box = fig.add_axes([left_margin, box_bottom, bull_width, box_height])
bull_box.barh(0, bull_share, height=1, color=BULL_FILL_COLOR, edgecolor=BULL_EDGE_COLOR)
bull_box.set_xlim(0,1)
bull_box.axis('off')
bull_box.text(0.5, 0, f"{bull_share:.1%}", ha='center', va='center', color='white', fontsize=14)
bear_box = fig.add_axes([bear_left, box_bottom, bull_width, box_height])
bear_box.barh(0, bear_share, height=1, color=BEAR_FILL_COLOR, edgecolor=BEAR_EDGE_COLOR)
bear_box.set_xlim(0,1)
bear_box.axis('off')
bear_box.text(0.5, 0, f"{bear_share:.1%}", ha='center', va='center', color='white', fontsize=14)
plt.tight_layout()
plt.show()
Figure 1. A dual-sided price-volume chart for PLTR. This shows. cumulative bullish (left) and bearish (right) volume by price over the last 180 daily bars. Volume-weighted metrics reveal zones of concentrated conviction.
2.5. How to Read the Chart
The left side displays cumulative bullish volume, concentrated heavily between $80 and $100.
The green dotted line shows the single price level with the highest bullish volume.
This cluster aligns closely with the bullish VWAP and median. This confirms it as a high-conviction zone where buyers repeatedly stepped in.
On the right, the bearish side shows a broader, flatter profile. Sellers were active between $100 and $110, but with less focus.
The red dotted line indicates the price with the highest bearish volume, yet it sits within a more distributed range.
The 60.7% to 39.3% volume split favors bulls not just in total volume, but in decisiveness.
The sharp curve on the left signals where buyers overwhelmed sellers across multiple sessions.
4. Limitations and Assumptions
This model offers insight, but there are tradeoffs. It simplifies the market to make directional pressure more visible with easily accesible data.
First, it assumes candle direction reflects trader intent. A green candle counts as bullish, but not every up-close day results from active buying.
Some are driven by gaps or passive drift. This means the volume split reflects outcomes, not orders. There’s no direct access to buyer or seller identities, only price behavior.
Second, it uses end-of-day closing prices to assign volume to price buckets. It ignores the intra-candle dynamics that may shape volume flow.
This is fine for broader structure, but less precise than true volume-at-price tools built on tick data.
Subscribe to our premium content to read the rest.
Become a paying subscriber to get access to this post and other subscriber-only content.
Upgrade





