Source code for vistock.mpl.profile

"""
Visualize a Volume Profile (or Turnover Profile) for a stock.
"""
__software__ = "Profile 2-split with mplfinace"
__version__ = "3.3"
__author__ = "York <york.jong@gmail.com>"
__date__ = "2023/02/02 (initial version) ~ 2024/09/05 (last revision)"

__all__ = [
    'Volume',   # Volume Profile, i.e., PBV (Price-by-Volume) or Volume-by-Price
    'Turnover', # Turnover Profile
]

import yfinance as yf
import matplotlib.pyplot as plt
import mplfinance as mpf

from .. import tw
from .. import file_utils
from ..utils import MarketColorStyle, decide_market_color_style
from . import mpf_utils as mpfu


def _plot(df, mpf_style, profile_field='Volume', period='1y', interval='1d',
          ma_nitems=(5, 10, 20, 50, 150), vma_nitems=50,
          total_bins=42, legend_loc='best', hides_nontrading=True):
    # Calculate price moving average
    for n in ma_nitems:
        df[f'MA {n}'] = df['Close'].rolling(window=n).mean()
    colors = ('orange', 'red', 'green', 'blue', 'cyan', 'magenta', 'yellow')

    # Calculate volume moving averaage
    df[f'VMA {vma_nitems}'] = df['Volume'].rolling(window=vma_nitems).mean()

    # Create subplots
    addplot = [
        # Plot of Price Moving Average
        *[mpf.make_addplot(df[f'MA {n}'], panel=0, label=f'MA {n}', color=c)
            for n, c in zip(ma_nitems, colors)],

        # Plot of Volume Moving Average
        mpf.make_addplot(df[f'VMA {vma_nitems}'], panel=1,
                         label=f'VMA {vma_nitems}', color='purple'),
    ]

    # Plot candlesticks MA, volume, volume MA, RSI
    fig, axes = mpf.plot(
        df, type='candle',              # candlesticks
        volume=True, addplot=addplot,   # MA, volume, volume MA
        figratio=(2, 1), figscale=1.2,
        style=mpf_style,
        show_nontrading=not hides_nontrading,
        returnfig=True,
    )
    # Set location of legends
    for ax in axes:
        if ax.legend_:
            ax.legend(loc=legend_loc)

    # Convert datetime index to string format suitable for display
    if interval.endswith('m') or interval.endswith('h'):
        df.index = df.index.strftime('%Y-%m-%d %H:%M')
    else:
        df.index = df.index.strftime('%Y-%m-%d')

    # Add Profile (e.g., Volume Profile or Turnover Profile)
    bin_size = (max(df['High']) - min(df['Low'])) / total_bins
    bin_round = lambda x: bin_size * round(x / bin_size)
    bin = df[profile_field].groupby(
            df['Close'].apply(lambda x: bin_round(x))).sum()
    ax = fig.add_axes(axes[0].get_position(), sharey=axes[0], frameon=False)
    ax.barh(
        y=bin.keys(),       # price
        width=bin.values,   # bin comulative volume/turnover
        height=0.75*bin_size,
        align='center',
        color='cyan',
        alpha=0.2
    )

    # Set x ticks of the Profile
    ax.set_xlim(right=1.2*max(bin.values))
    ax.tick_params(top=True, labeltop=True, bottom=False, labelbottom=False)

    # Set x label of the Profile
    ax.set_xlabel(f'Bin Cumulative {profile_field}')
    if profile_field == 'Turnover':
        ax.set_xlabel('Bin Cumulative Turnover (Price*Volume)')
    ax.xaxis.set_label_position('top')

    # Disable gridlines on the secondary ax
    ax.grid(False)

    return fig


[docs] class Volume: """Volume Profile, i.e., PBV (Price-by-Volume) or Volume-by-Price """
[docs] @staticmethod def plot(symbol='TSLA', period='1y', interval='1d', ma_nitems=(5, 10, 20, 50, 150), vma_nitems=50, total_bins=42, legend_loc='best', market_color_style=MarketColorStyle.AUTO, style='binancedark', hides_nontrading=True, out_dir='out'): """Plot a price-by-volume, PBV (also called volume profile) figure for a given stock. Here the PBV is overlaid with the price subplot. This figure consists of two subplots: a price subplot and a volume subplot. The former includes price candlesticks, price moving average lines, while the latter includes a trading volume histogram and a volume moving average line. Parameters ---------- symbol: str the stock symbol. period: str, optional the period data to download (default is '1y'). Valid values are 1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, 10y, ytd, max. - d -- days - mo -- monthes - y -- years - ytd -- year to date - max -- all data interval: str, optional the interval of an OHLC item (default is '1d'). Valid values are 1m, 2m, 5m, 15m, 30m, 60m, 90m, 1h, 1d, 5d, 1wk, 1mo, 3mo. - m -- minutes - h -- hours - wk -- weeks - mo -- monthes Intraday data cannot extend last 60 days: - 1m - max 7 days within last 30 days - up to 90m - max 60 days - 60m, 1h - max 730 days (yes 1h is technically < 90m but this what Yahoo does) ma_nitems: sequence of int a sequence to list the number of data items to calclate moving averges. vma_nitems: int the number of data items to calculate the volume moving average. total_bins: int the number of bins to calculate comulative volume for bins. legend_loc: str, optional the location of the legend (default is 'best'). Valid locations are - 'best' - 'upper right' - 'upper left' - 'lower left' - 'lower right' - 'right' - 'center left' - 'center right' - 'lower center' - 'upper center' - 'center' market_color_style: MarketColorStyle, optional The market color style to use. Default is MarketColorStyle.AUTO. style: str, optional The chart style to use. Common styles include: - 'yahoo': Yahoo Finance style - 'charles': Charles style - 'tradingview': TradingView style - 'binance': Binance style - 'binancedark': Binance dark mode style - 'mike': Mike style (dark mode) - 'nightclouds': Dark mode with sleek appearance - 'checkers': Checkered style - 'ibd': Investor's Business Daily style - 'sas': SAS style - 'starsandstripes': Stars and Stripes style - 'kenan': Kenan style - 'blueskies': Blue Skies style - 'brasil': Brasil style Default is 'yahoo'. hides_nontrading: bool, optional Whether to hide non-trading periods. Default is True. out_dir: str, optional the output directory for saving figure. """ # Download stock data ticker = tw.as_yfinance(symbol) df = yf.Ticker(ticker).history(period=period, interval=interval) # Plot mc_style = decide_market_color_style(ticker, market_color_style) mpf_style = mpfu.decide_mpf_style(base_mpf_style=style, market_color_style=mc_style) fig = _plot(df, mpf_style, 'Volume', period, interval, ma_nitems, vma_nitems, total_bins, legend_loc, hides_nontrading) fig.suptitle( f"{symbol} - {interval} ({df.index[0]} to {df.index[-1]})", ) # Show the figure mpf.show() # Write the figure to an PNG file out_dir = file_utils.make_dir(out_dir) fn = file_utils.gen_fn_info(ticker, interval, df.index[-1], 'volume_prf') fig.savefig(f'{out_dir}/{fn}.png')
[docs] class Turnover: '''Turnover Profile Here "turnover" means "trading value" (= price * volume) '''
[docs] @staticmethod def plot(symbol='TSLA', period='1y', interval='1d', ma_nitems=(5, 10, 20, 50, 150), vma_nitems=50, total_bins=42, legend_loc='best', market_color_style=MarketColorStyle.AUTO, style='binancedark', hides_nontrading=True, out_dir='out'): """Plot a turnover profile figure for a given stock. Here the provile is overlaid with the price subplot. This figure consists of two subplots: a price subplot and a volume subplot. The former includes price candlesticks, price moving average lines, while the latter includes a trading volume histogram and a volume moving average line. Parameters ---------- symbol: str the stock symbol. period: str, optional the period data to download (default is '1y'). Valid values are 1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, 10y, ytd, max. - d -- days - mo -- monthes - y -- years - ytd -- year to date - max -- all data interval: str, optional the interval of an OHLC item (default is '1d'). Valid values are 1m, 2m, 5m, 15m, 30m, 60m, 90m, 1h, 1d, 5d, 1wk, 1mo, 3mo. - m -- minutes - h -- hours - wk -- weeks - mo -- monthes Intraday data cannot extend last 60 days: - 1m - max 7 days within last 30 days - up to 90m - max 60 days - 60m, 1h - max 730 days (yes 1h is technically < 90m but this what Yahoo does) ma_nitems: sequence of int a sequence to list the number of data items to calclate moving averges. vma_nitems: int the number of data items to calculate the volume moving average. total_bins: int the number of bins to calculate comulative volume for bins. legend_loc: str, optional the location of the legend (default is 'best'). Valid locations are - 'best' - 'upper right' - 'upper left' - 'lower left' - 'lower right' - 'right' - 'center left' - 'center right' - 'lower center' - 'upper center' - 'center' market_color_style: MarketColorStyle, optional The market color style to use. Default is MarketColorStyle.AUTO. style: str, optional The chart style to use. Common styles include: - 'yahoo': Yahoo Finance style - 'charles': Charles style - 'tradingview': TradingView style - 'binance': Binance style - 'binancedark': Binance dark mode style - 'mike': Mike style (dark mode) - 'nightclouds': Dark mode with sleek appearance - 'checkers': Checkered style - 'ibd': Investor's Business Daily style - 'sas': SAS style - 'starsandstripes': Stars and Stripes style - 'kenan': Kenan style - 'blueskies': Blue Skies style - 'brasil': Brasil style Default is 'yahoo'. hides_nontrading: bool, optional Whether to hide non-trading periods. Default is True. out_dir: str, optional the output directory for saving figure. """ # Download stock data ticker = tw.as_yfinance(symbol) df = yf.Ticker(ticker).history(period=period, interval=interval) df['Turnover'] = df['Close'] * df['Volume'] # Plot mc_style = decide_market_color_style(ticker, market_color_style) mpf_style = mpfu.decide_mpf_style(base_mpf_style=style, market_color_style=mc_style) fig = _plot(df, mpf_style, 'Turnover', period, interval, ma_nitems, vma_nitems, total_bins, legend_loc, hides_nontrading) fig.suptitle( f"{symbol} - {interval} ({df.index[0]} to {df.index[-1]})", ) # Show the figure mpf.show() # Write the figure to an PNG file out_dir = file_utils.make_dir(out_dir) fn = file_utils.gen_fn_info(ticker, interval, df.index[-1], 'turnover_prf') fig.savefig(f'{out_dir}/{fn}.png')
if __name__ == '__main__': mpfu.use_mac_chinese_font() Volume.plot('TSLA') Volume.plot('台積電') Turnover.plot('台積電')