2025, Sep 26 17:33
GridSpec और shared y‑axis के साथ रेजीम‑आधारित कैंडलस्टिक वॉल्यूम प्रोफ़ाइल को एक कैनवास पर प्लॉट करें
Python/Matplotlib में टाइम‑सीरीज़ को रेजीम में बाँटकर 5‑मिनट कैंडलस्टिक और वॉल्यूम प्रोफ़ाइल को GridSpec व shared y‑axis के साथ एक ही फ़िगर में प्लॉट करने की विधि.
जब आप समय-श्रृंखला को रेजीम्स में बांटकर हर हिस्से के लिए वॉल्यूम प्रोफ़ाइल बनाते हैं, तो उन्हें अलग-अलग प्लॉट करना बाजार की क्रमबद्ध प्रकृति को छिपा देता है। आपको वास्तव में एक ही कैनवास चाहिए, जो x-अक्ष पर हर रेजीम को क्रम से एक-दूसरे के बाद दिखाए और कीमत का साझा पैमाना बनाए रखे। नीचे बताया गया है कि बिना गणना-तर्क बदले तीन अलग-थलग प्लॉट्स से एक लगातार, साथ-साथ रखा गया लेआउट कैसे बनाया जाए।
समस्या की रूपरेखा और प्रति-रेजीम प्लॉटिंग जिससे विखंडन होता है
डेटासेट में OHLCV बार्स हैं, जिनमें Regime कॉलम का मान 1, 2 या 3 होता है। नीचे दिया गया कोड कैंडलस्टिक के लिए डेटा को 5‑मिनट बार्स में समूहित करता है, हर रेजीम के लिए मूल 1‑मिनट डेटा से वॉल्यूम प्रोफ़ाइल बनाता है, और हर रेजीम को अलग फ़िगर में प्लॉट करता है। नतीजा यह होता है कि x‑अक्ष पर एक सतत दृश्य की जगह तीन अलग-अलग चार्ट बन जाते हैं।
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
payload = {
    'Date' : ['2025-08-22 16:00:00','2025-08-22 16:01:00','2025-08-22 16:02:00','2025-08-22 16:03:00','2025-08-22 16:04:00','2025-08-22 16:05:00','2025-08-22 16:06:00','2025-08-22 16:07:00','2025-08-22 16:08:00','2025-08-22 16:09:00','2025-08-22 16:10:00','2025-08-22 16:11:00','2025-08-22 16:12:00','2025-08-22 16:13:00','2025-08-22 16:14:00','2025-08-22 16:15:00','2025-08-22 16:16:00','2025-08-22 16:17:00','2025-08-22 16:18:00','2025-08-22 16:19:00','2025-08-22 16:20:00','2025-08-22 16:21:00','2025-08-22 16:22:00','2025-08-22 16:23:00','2025-08-22 16:24:00'],
    'Open': [11717.9,11717.95,11716.6,11717.4,11719.5,11727.25,11725.55,11724.35,11725.45,11724.15,11728.2,11726.6,11727.6,11729.1,11724.1,11722.8,11721.8,11720.8,11718.8,11716.7,11716.9,11722.5,11721.6,11727.8,11728.1],
    'Low': [11715.9,11716,11715.35,11716.45,11719.5,11724.3,11723.55,11723.15,11723.85,11724.15,11725.2,11726.6,11727.6,11724.2,11722.6,11721.6,11719.7,11715.8,11716.5,11716,11716.9,11721.3,11721.4,11726.35,11727],
    'High': [11718.1,11718.1,11717.9,11719.4,11727.15,11727.45,11726,11725.65,11727.2,11727.85,11728.2,11728.7,11729.5,11729.1,11725.5,11723.9,11722,11720.8,11719.8,11717.7,11722.9,11724.3,11727.8,11728.3,11728.8],
    'Close' : [11718.05,11716.5,11717,11719.3,11727.15,11725.65,11724.15,11725.35,11724.05,11727.65,11726.7,11727.8,11729.2,11724.2,11722.6,11721.7,11721.2,11718.7,11716.6,11716.8,11722.6,11721.5,11727.6,11728,11727.2],
    'Volume': [130,88,125,93,154,102,118,92,105,116,84,88,108,99,82,109,98,130,71,86,96,83,80,93,73],
    'Regime': [1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,3,3,3,3,3,3,3],
}
frame = pd.DataFrame(data=payload)
frame['Date'] = pd.to_datetime(frame['Date'])
frame.set_index('Date', inplace=True)
five_min = frame.resample('5T').agg({'Open': 'first','High': 'max','Low': 'min','Close': 'last','Volume': 'sum','Regime': 'last'})
five_min = five_min.dropna()
five_min['date'] = five_min.index.date
frame['date'] = frame.index.date
for regime_val in frame['Regime'].unique():
    slice_5 = five_min[five_min['Regime'] == regime_val]
    slice_1 = frame[frame['Regime'] == regime_val]
    if slice_5.empty:
        continue
    fig_one, axis = plt.subplots(figsize=(10, 6))
    slice_5_reset = slice_5.reset_index()
    x_idx = np.arange(len(slice_5_reset))
    p_min = slice_1['Low'].min()
    p_max = slice_1['High'].max()
    bin_count = 200
    price_bins = np.linspace(p_min, p_max, bin_count + 1)
    step = price_bins[1] - price_bins[0]
    centers = (price_bins[:-1] + price_bins[1:]) / 2
    vp = np.zeros(bin_count)
    for _, r in slice_1.iterrows():
        lo = r['Low']
        hi = r['High']
        vol = r['Volume']
        if hi == lo:
            bidx = np.digitize(lo, price_bins) - 1
            if 0 <= bidx < bin_count:
                vp[bidx] += vol
        else:
            vpu = vol / (hi - lo)
            sb = np.digitize(lo, price_bins)
            eb = np.digitize(hi, price_bins)
            for b in range(sb, eb + 1):
                if b > 0 and b <= bin_count:
                    b_start = price_bins[b - 1]
                    b_end = price_bins[b]
                    seg_start = max(lo, b_start)
                    seg_end = min(hi, b_end)
                    part = (seg_end - seg_start) * vpu
                    vp[b - 1] += part
    span = len(slice_5_reset)
    if vp.max() > 0:
        vp_scaled = (vp / vp.max()) * span
    else:
        vp_scaled = vp
    poc_i = np.argmax(vp)
    poc_val = centers[poc_i]
    axis.fill_betweenx(centers, 0, vp_scaled, color='blue', alpha=0.3, step='mid')
    axis.axhline(poc_val, color='red', linestyle='-', linewidth=1)
    body_w = 0.6
    for i in range(len(slice_5_reset)):
        o = slice_5_reset['Open'][i]
        h = slice_5_reset['High'][i]
        l = slice_5_reset['Low'][i]
        c = slice_5_reset['Close'][i]
        if c > o:
            clr = 'green'
            base = o
            hgt = c - o
        else:
            clr = 'red'
            base = c
            hgt = o - c
        axis.vlines(x_idx[i], l, h, color='black', linewidth=0.5)
        axis.bar(x_idx[i], hgt, body_w, base, color=clr, edgecolor='black')
    axis.set_xlim(-1, span + 1)
    axis.set_ylim(p_min - step, p_max + step)
    axis.set_xticks(x_idx)
    axis.set_xticklabels(slice_5_reset['Date'].dt.strftime('%H:%M'), rotation=45)
    axis.set_title(f'30-min Candlestick with Volume Profile - Regime: {regime_val}')
    axis.set_xlabel('Time')
    axis.set_ylabel('Price')
    plt.tight_layout()
    plt.show()
प्लॉट एक ही व्यू में क्यों नहीं बैठते
क्योंकि हर रेजीम अपनी-अपनी फ़िगर में रेंडर होता है, Matplotlib को न तो उन्हें साथ-साथ रखने का निर्देश मिलता है और न ही एक साझा y‑अक्ष देने का। पैनल भले मिलते-जुलते दिखें, अलग फ़िगर्स मिलकर एक निरंतर टाइमलाइन नहीं बनाते। साथ ही, लेबल और शीर्षक बार-बार दोहरते हैं, क्षैतिज जगह खर्च होती है और दृश्य तुलना कमजोर पड़ती है।
GridSpec लेआउट और साझा अक्षों के साथ एक ही फ़िगर बनाएं
समाधान यह है कि एक ही मास्टर फ़िगर बनाएं, तीन‑कॉलम ग्रिड तैयार करें और हर रेजीम की ड्रॉइंग कमांड्स को उसके संबंधित एक्सिस पर भेजें। GridSpec पैनलों के बीच की खाली जगह हटाता है, sharey कीमत के पैमाने को समान रखता है, suptitle साझा शीर्षक संभालता है और छोटे शीर्षक हर रेजीम को चिह्नित करते हैं। plt.show() को अंत में, तब ही कॉल करें जब तीनों पैनल बन चुके हों।
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
payload = {
    'Date' : ['2025-08-22 16:00:00','2025-08-22 16:01:00','2025-08-22 16:02:00','2025-08-22 16:03:00','2025-08-22 16:04:00','2025-08-22 16:05:00','2025-08-22 16:06:00','2025-08-22 16:07:00','2025-08-22 16:08:00','2025-08-22 16:09:00','2025-08-22 16:10:00','2025-08-22 16:11:00','2025-08-22 16:12:00','2025-08-22 16:13:00','2025-08-22 16:14:00','2025-08-22 16:15:00','2025-08-22 16:16:00','2025-08-22 16:17:00','2025-08-22 16:18:00','2025-08-22 16:19:00','2025-08-22 16:20:00','2025-08-22 16:21:00','2025-08-22 16:22:00','2025-08-22 16:23:00','2025-08-22 16:24:00'],
    'Open': [11717.9,11717.95,11716.6,11717.4,11719.5,11727.25,11725.55,11724.35,11725.45,11724.15,11728.2,11726.6,11727.8,11729.2,11724.1,11722.8,11721.8,11720.8,11718.8,11716.7,11716.9,11722.5,11721.6,11727.8,11728.1],
    'Low': [11715.9,11716,11715.35,11716.45,11719.5,11724.3,11723.55,11723.15,11723.85,11724.15,11725.2,11726.6,11727.6,11724.2,11722.6,11721.6,11719.7,11715.8,11716.5,11716,11716.9,11721.3,11721.4,11726.35,11727],
    'High': [11718.1,11718.1,11717.9,11719.4,11727.15,11727.45,11726,11725.65,11727.2,11727.85,11728.2,11728.7,11729.5,11729.1,11725.5,11723.9,11722,11720.8,11719.8,11717.7,11722.9,11724.3,11727.8,11728.3,11728.8],
    'Close' : [11718.05,11716.5,11717,11719.3,11727.15,11725.65,11724.15,11725.35,11724.05,11727.65,11726.7,11727.8,11729.2,11724.2,11722.6,11721.7,11721.2,11718.7,11716.6,11716.8,11722.6,11721.5,11727.6,11728,11727.2],
    'Volume': [130,88,125,93,154,102,118,92,105,116,84,88,108,99,82,109,98,130,71,86,96,83,80,93,73],
    'Regime': [1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,3,3,3,3,3,3,3],
}
frame = pd.DataFrame(data=payload)
frame['Date'] = pd.to_datetime(frame['Date'])
frame.set_index('Date', inplace=True)
five_min = frame.resample('5T').agg({'Open': 'first','High': 'max','Low': 'min','Close': 'last','Volume': 'sum','Regime': 'last'})
five_min = five_min.dropna()
five_min['date'] = five_min.index.date
frame['date'] = frame.index.date
canvas = plt.figure(figsize=(30, 6))
layout = canvas.add_gridspec(nrows=1, ncols=3, hspace=0, wspace=0)
axarr = layout.subplots(sharey=True)
canvas.suptitle('30-min Candlestick with Volume Profile')
for idx_reg, regime_val in enumerate(frame['Regime'].unique()]):
    slice_5 = five_min[five_min['Regime'] == regime_val]
    slice_1 = frame[frame['Regime'] == regime_val]
    if slice_5.empty:
        continue
    slice_5_reset = slice_5.reset_index()
    x_idx = np.arange(len(slice_5_reset))
    p_min = slice_1['Low'].min()
    p_max = slice_1['High'].max()
    bin_count = 200
    price_bins = np.linspace(p_min, p_max, bin_count + 1)
    step = price_bins[1] - price_bins[0]
    centers = (price_bins[:-1] + price_bins[1:]) / 2
    vp = np.zeros(bin_count)
    for _, r in slice_1.iterrows():
        lo = r['Low']
        hi = r['High']
        vol = r['Volume']
        if hi == lo:
            bidx = np.digitize(lo, price_bins) - 1
            if 0 <= bidx < bin_count:
                vp[bidx] += vol
        else:
            vpu = vol / (hi - lo)
            sb = np.digitize(lo, price_bins)
            eb = np.digitize(hi, price_bins)
            for b in range(sb, eb + 1):
                if b > 0 and b <= bin_count:
                    b_start = price_bins[b - 1]
                    b_end = price_bins[b]
                    seg_start = max(lo, b_start)
                    seg_end = min(hi, b_end)
                    part = (seg_end - seg_start) * vpu
                    vp[b - 1] += part
    span = len(slice_5_reset)
    if vp.max() > 0:
        vp_scaled = (vp / vp.max()) * span
    else:
        vp_scaled = vp
    poc_i = np.argmax(vp)
    poc_val = centers[poc_i]
    axarr[idx_reg].fill_betweenx(centers, 0, vp_scaled, color='blue', alpha=0.3, step='mid')
    axarr[idx_reg].axhline(poc_val, color='red', linestyle='-', linewidth=1)
    body_w = 0.6
    for i in range(len(slice_5_reset)):
        o = slice_5_reset['Open'][i]
        h = slice_5_reset['High'][i]
        l = slice_5_reset['Low'][i]
        c = slice_5_reset['Close'][i]
        if c > o:
            clr = 'green'
            base = o
            hgt = c - o
        else:
            clr = 'red'
            base = c
            hgt = o - c
        axarr[idx_reg].vlines(x_idx[i], l, h, color='black', linewidth=0.5)
        axarr[idx_reg].bar(x_idx[i], hgt, body_w, base, color=clr, edgecolor='black')
    axarr[idx_reg].set_xlim(-1, span + 1)
    axarr[idx_reg].set_ylim(p_min - step, p_max + step)
    axarr[idx_reg].set_xticks(x_idx)
    axarr[idx_reg].set_xticklabels(slice_5_reset['Date'].dt.strftime('%H:%M'), rotation=45)
    axarr[idx_reg].set_title(f'Regime: {regime_val}')
    axarr[idx_reg].set_xlabel('Time')
    axarr[idx_reg].set_ylabel('Price')
for a in axarr:
    a.label_outer()
plt.tight_layout()
plt.show()
यह करने की अहमियत
तीन पैनल वाला एक ही फ़िगर रेजीम्स को एक ही अनुक्रम के हिस्सों की तरह दिखाता है, अलग-थलग चार्ट्स की तरह नहीं। साझा y‑अक्ष कीमत के पैमाने को स्थिर रखता है, पैनलों के बीच की खाली जगह खत्म होती है, और सामान्य तत्व suptitle तथा बाहरी लेबल्स में चले जाते हैं—पठनीयता बेहतर होती है। सबसे महत्त्वपूर्ण, x‑अक्ष पर क्रम अब इच्छित प्रगति दिखाता है: Regime 1 के बाद Regime 2, और उसके बाद Regime 3।
मुख्य बातें
जब रेजीम जैसे श्रेणीबद्ध हिस्सों में लगातार दृश्य चाहिए, अलग-अलग फ़िगर्स न बनाएं। एक पेरेंट फ़िगर बनाएं, उसे GridSpec से विभाजित करें, मात्रात्मक तुलना के लिए y‑अक्ष साझा रखें, और हर हिस्से को उसके दिए गए एक्सिस पर ड्रॉ करें। प्लॉटिंग की कॉल को अंत तक रोके रखें ताकि सब कुछ एक बार में रेंडर हो। इससे वॉल्यूम प्रोफ़ाइल की मूल गणना जस की तस रहती है, और आपको बाएँ से दाएँ स्वाभाविक रूप से पढ़ा जाने वाला एक सुसंगत लेआउट मिलता है।
यह लेख StackOverflow पर प्रश्न (लेखक: Giampaolo Levorato) और Aadvik के उत्तर पर आधारित है।