From 9b09626f93d4323804d70000ee83e7d43fdec747 Mon Sep 17 00:00:00 2001 From: sanykiv Date: Fri, 17 Jul 2026 23:08:26 +0500 Subject: [PATCH 1/3] ENH: add volume_color_updown_dependancy kwarg (Open vs Close / Previous Close / Previous Volume) --- src/mplfinance/_styles.py | 13 +++++++++++++ src/mplfinance/plotting.py | 23 +++++++++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/mplfinance/_styles.py b/src/mplfinance/_styles.py index 84eb7fd5..1c540242 100644 --- a/src/mplfinance/_styles.py +++ b/src/mplfinance/_styles.py @@ -264,6 +264,16 @@ def _valid_make_marketcolors_kwargs(): 'Description' : 'True/False volume color depends on price change from previous day', 'Validator' : lambda value: isinstance(value,bool) }, + 'volume_color_updown_dependancy' + : { 'Default' : None, + 'Description' : 'What the up/down color of the volume bars depends on: ' + '"Open vs Close" (default), "Previous Close", or "Previous Volume". ' + 'Matching is case- and space-insensitive.', + 'Validator' : lambda value: value is None or + ( isinstance(value,str) and + value.lower().replace(' ','') in + ('openvsclose','previousclose','previousvolume') ) }, + 'inherit' : { 'Default' : False, 'Description' : 'inherit color from base_mpf_style for: edge,volume,ohlc,wick', @@ -355,6 +365,9 @@ def _check_and_set_mktcolor(candle,**kwarg): if config['vcdopcod'] is not None: marketcolors.update({'vcdopcod':config['vcdopcod']}) + if config['volume_color_updown_dependancy'] is not None: + marketcolors.update({'volume_color_updown_dependancy':config['volume_color_updown_dependancy']}) + return marketcolors def write_style_file(style,filename): diff --git a/src/mplfinance/plotting.py b/src/mplfinance/plotting.py index 3f5350e8..b9bbe189 100644 --- a/src/mplfinance/plotting.py +++ b/src/mplfinance/plotting.py @@ -676,7 +676,26 @@ def plot( data, **kwargs ): mc = style['marketcolors'] vup,vdown = mc['volume'].values() #-- print('vup,vdown=',vup,vdown) - vcolors = _updown_colors(vup, vdown, opens, closes, use_prev_close=style['marketcolors']['vcdopcod']) + + # How the up/down color of each volume bar is decided: + # 'openvsclose' : today's open vs close (default, same as the candles) + # 'previousclose' : today's close vs the previous close (legacy `vcdopcod=True`) + # 'previousvolume' : today's volume vs the previous volume + _vcdep = mc.get('volume_color_updown_dependancy', None) + if _vcdep is not None: + _vcmode = _vcdep.lower().replace(' ', '') + elif mc.get('vcdopcod', False): + _vcmode = 'previousclose' # backward compatibility with legacy `vcdopcod` + else: + _vcmode = 'openvsclose' + + def _volume_updown_colors(upcolor, downcolor): + if _vcmode == 'previousvolume': + return _updown_colors(upcolor, downcolor, volumes, volumes, use_prev_close=True) + return _updown_colors(upcolor, downcolor, opens, closes, + use_prev_close=(_vcmode == 'previousclose')) + + vcolors = _volume_updown_colors(vup, vdown) #-- print('len(vcolors),len(opens),len(closes)=',len(vcolors),len(opens),len(closes)) #-- print('vcolors=',vcolors) @@ -687,7 +706,7 @@ def plot( data, **kwargs ): if mc['volume'] == mc['vcedge']: edgecolors = _adjust_color_brightness(vcolors,0.90) elif veup != vedown: - edgecolors = _updown_colors(veup, vedown, opens, closes, use_prev_close=style['marketcolors']['vcdopcod']) + edgecolors = _volume_updown_colors(veup, vedown) else: edgecolors = veup From ce97c2cfeac7f5a276ad871e2051695cfcc721b0 Mon Sep 17 00:00:00 2001 From: sanykiv Date: Fri, 17 Jul 2026 23:15:07 +0500 Subject: [PATCH 2/3] TST: add tests for volume_color_updown_dependancy (modes, normalization, vcdopcod back-compat) --- tests/test_volume_color_updown_dependancy.py | 72 ++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 tests/test_volume_color_updown_dependancy.py diff --git a/tests/test_volume_color_updown_dependancy.py b/tests/test_volume_color_updown_dependancy.py new file mode 100644 index 00000000..0da6ac51 --- /dev/null +++ b/tests/test_volume_color_updown_dependancy.py @@ -0,0 +1,72 @@ +""" +Regression tests for the `volume_color_updown_dependancy` market-colors setting, +which controls how the up/down color of each volume bar is decided: + + "Open vs Close" (default) : today's open vs today's close (same as candles) + "Previous Close" : today's close vs the previous close + "Previous Volume" : today's volume vs the previous volume + +Value matching is case- and space-insensitive. +Also verifies backward compatibility with the legacy `vcdopcod` setting. +""" + +import numpy as np +import pandas as pd +import mplfinance as mpf +import matplotlib.pyplot as plt +import pytest + +print('mpf.__version__ =', mpf.__version__) # for the record + + +def _sample_df(): + idx = pd.date_range('2024-01-01', periods=8, freq='D') + close = np.array([10, 11, 10.5, 12, 11.8, 13, 12.5, 14.0]) + # Volume deliberately rises/falls independently of price, so that the + # 'Previous Volume' coloring is distinguishable from the price-based modes. + vol = np.array([100, 80, 120, 90, 150, 60, 200, 50.0]) + df = pd.DataFrame({'Open': close - 0.2, 'High': close + 0.5, + 'Low': close - 0.5, 'Close': close, 'Volume': vol}, index=idx) + df.index.name = 'Date' + return df + + +def _volume_bar_colors(df, **mktcolor_kwargs): + """Return the list of face colors of the volume bars for the given marketcolors.""" + mc = mpf.make_marketcolors(up='g', down='r', volume={'up': 'g', 'down': 'r'}, + **mktcolor_kwargs) + style = mpf.make_mpf_style(marketcolors=mc) + fig, axlist = mpf.plot(df, type='candle', volume=True, style=style, returnfig=True) + volume_ax = axlist[2] # panel holding the volume bars + colors = [tuple(np.round(patch.get_facecolor(), 3)) for patch in volume_ax.patches] + plt.close(fig) + return colors + + +def test_volume_color_modes_differ(): + df = _sample_df() + open_vs_close = _volume_bar_colors(df, volume_color_updown_dependancy='Open vs Close') + previous_vol = _volume_bar_colors(df, volume_color_updown_dependancy='Previous Volume') + + # The whole point of the feature: coloring by volume differs from the default. + assert previous_vol != open_vs_close + assert len(previous_vol) == len(df) + + +def test_case_and_space_insensitive(): + df = _sample_df() + canonical = _volume_bar_colors(df, volume_color_updown_dependancy='Previous Volume') + for variant in ('previous volume', 'PREVIOUSVOLUME', ' Previous Volume '): + assert _volume_bar_colors(df, volume_color_updown_dependancy=variant) == canonical + + +def test_backward_compatible_with_vcdopcod(): + df = _sample_df() + legacy = _volume_bar_colors(df, vcdopcod=True) + explicit = _volume_bar_colors(df, volume_color_updown_dependancy='Previous Close') + assert legacy == explicit + + +def test_invalid_value_raises(): + with pytest.raises(Exception): + mpf.make_marketcolors(volume_color_updown_dependancy='not a real mode') From b55ce4ad63d0f3ae1f544cb3a804fabfbb0d2289 Mon Sep 17 00:00:00 2001 From: sanykiv Date: Sat, 18 Jul 2026 10:03:55 +0500 Subject: [PATCH 3/3] ENH: rename to volume_color_updown_dependency (spelling) + expose via plot() and make_mpf_style() --- src/mplfinance/_styles.py | 36 ++++-- src/mplfinance/plotting.py | 12 +- tests/test_volume_color_updown_dependancy.py | 72 ------------ tests/test_volume_color_updown_dependency.py | 115 +++++++++++++++++++ 4 files changed, 155 insertions(+), 80 deletions(-) delete mode 100644 tests/test_volume_color_updown_dependancy.py create mode 100644 tests/test_volume_color_updown_dependency.py diff --git a/src/mplfinance/_styles.py b/src/mplfinance/_styles.py index 1c540242..771c5e4f 100644 --- a/src/mplfinance/_styles.py +++ b/src/mplfinance/_styles.py @@ -125,6 +125,12 @@ def _valid_make_mpf_style_kwargs(): 'Description' : 'name for this style; useful when calling `mpf.write_style_file(style,filename)`', 'Validator' : lambda value: isinstance(value,str) }, + 'volume_color_updown_dependency' + : { 'Default' : None, + 'Description' : 'How volume bar up/down colors are decided: ' + '"Open vs Close" (default), "Previous Close", or "Previous Volume".', + 'Validator' : _valid_volume_color_updown_dependency }, + } _validate_vkwargs_dict(vkwargs) @@ -135,6 +141,18 @@ def _valid_make_mpf_style_kwargs(): def available_styles(): return list(_styles.keys()) +def _valid_volume_color_updown_dependency(value): + ''' + Validate a `volume_color_updown_dependency` value. Allowed values + (matched case- and space-insensitively) are: "Open vs Close" (default), + "Previous Close", and "Previous Volume". + ''' + return ( value is None or + ( isinstance(value,str) and + value.lower().replace(' ','') in + ('openvsclose','previousclose','previousvolume') ) ) + + def make_mpf_style( **kwargs ): config = _process_kwargs(kwargs, _valid_make_mpf_style_kwargs()) if config['rc'] is not None and config['legacy_rc'] is not None: @@ -175,6 +193,13 @@ def make_mpf_style( **kwargs ): if style['marketcolors'] is None: style['marketcolors'] = _styles['default']['marketcolors'] + # `volume_color_updown_dependency` may be passed directly to make_mpf_style(); + # store it in marketcolors (copying first, so a shared/default dict is never mutated). + vcud = style.pop('volume_color_updown_dependency', None) + if vcud is not None: + style['marketcolors'] = dict(style['marketcolors']) + style['marketcolors']['volume_color_updown_dependency'] = vcud + return style def _valid_mpf_color_spec(value): @@ -264,15 +289,12 @@ def _valid_make_marketcolors_kwargs(): 'Description' : 'True/False volume color depends on price change from previous day', 'Validator' : lambda value: isinstance(value,bool) }, - 'volume_color_updown_dependancy' + 'volume_color_updown_dependency' : { 'Default' : None, 'Description' : 'What the up/down color of the volume bars depends on: ' '"Open vs Close" (default), "Previous Close", or "Previous Volume". ' 'Matching is case- and space-insensitive.', - 'Validator' : lambda value: value is None or - ( isinstance(value,str) and - value.lower().replace(' ','') in - ('openvsclose','previousclose','previousvolume') ) }, + 'Validator' : _valid_volume_color_updown_dependency }, 'inherit' : { 'Default' : False, @@ -365,8 +387,8 @@ def _check_and_set_mktcolor(candle,**kwarg): if config['vcdopcod'] is not None: marketcolors.update({'vcdopcod':config['vcdopcod']}) - if config['volume_color_updown_dependancy'] is not None: - marketcolors.update({'volume_color_updown_dependancy':config['volume_color_updown_dependancy']}) + if config['volume_color_updown_dependency'] is not None: + marketcolors.update({'volume_color_updown_dependency':config['volume_color_updown_dependency']}) return marketcolors diff --git a/src/mplfinance/plotting.py b/src/mplfinance/plotting.py index b9bbe189..795e9b8d 100644 --- a/src/mplfinance/plotting.py +++ b/src/mplfinance/plotting.py @@ -152,6 +152,12 @@ def _valid_plot_kwargs(): 'mco_faceonly' : { 'Default' : False, # If True: Override only the face of the candle 'Description' : 'True/False marketcolor_overrides only apply to face of candle.', 'Validator' : lambda value: isinstance(value,bool) }, + + 'volume_color_updown_dependency' + : { 'Default' : None, + 'Description' : 'How volume bar up/down colors are decided: ' + '"Open vs Close" (default), "Previous Close", or "Previous Volume".', + 'Validator' : _styles._valid_volume_color_updown_dependency }, 'no_xgaps' : { 'Default' : True, # None means follow default logic below: 'Description' : 'deprecated', @@ -681,7 +687,11 @@ def plot( data, **kwargs ): # 'openvsclose' : today's open vs close (default, same as the candles) # 'previousclose' : today's close vs the previous close (legacy `vcdopcod=True`) # 'previousvolume' : today's volume vs the previous volume - _vcdep = mc.get('volume_color_updown_dependancy', None) + # A `volume_color_updown_dependency` kwarg passed directly to `plot()` + # takes precedence over the same setting in the style's marketcolors. + _vcdep = config['volume_color_updown_dependency'] + if _vcdep is None: + _vcdep = mc.get('volume_color_updown_dependency', None) if _vcdep is not None: _vcmode = _vcdep.lower().replace(' ', '') elif mc.get('vcdopcod', False): diff --git a/tests/test_volume_color_updown_dependancy.py b/tests/test_volume_color_updown_dependancy.py deleted file mode 100644 index 0da6ac51..00000000 --- a/tests/test_volume_color_updown_dependancy.py +++ /dev/null @@ -1,72 +0,0 @@ -""" -Regression tests for the `volume_color_updown_dependancy` market-colors setting, -which controls how the up/down color of each volume bar is decided: - - "Open vs Close" (default) : today's open vs today's close (same as candles) - "Previous Close" : today's close vs the previous close - "Previous Volume" : today's volume vs the previous volume - -Value matching is case- and space-insensitive. -Also verifies backward compatibility with the legacy `vcdopcod` setting. -""" - -import numpy as np -import pandas as pd -import mplfinance as mpf -import matplotlib.pyplot as plt -import pytest - -print('mpf.__version__ =', mpf.__version__) # for the record - - -def _sample_df(): - idx = pd.date_range('2024-01-01', periods=8, freq='D') - close = np.array([10, 11, 10.5, 12, 11.8, 13, 12.5, 14.0]) - # Volume deliberately rises/falls independently of price, so that the - # 'Previous Volume' coloring is distinguishable from the price-based modes. - vol = np.array([100, 80, 120, 90, 150, 60, 200, 50.0]) - df = pd.DataFrame({'Open': close - 0.2, 'High': close + 0.5, - 'Low': close - 0.5, 'Close': close, 'Volume': vol}, index=idx) - df.index.name = 'Date' - return df - - -def _volume_bar_colors(df, **mktcolor_kwargs): - """Return the list of face colors of the volume bars for the given marketcolors.""" - mc = mpf.make_marketcolors(up='g', down='r', volume={'up': 'g', 'down': 'r'}, - **mktcolor_kwargs) - style = mpf.make_mpf_style(marketcolors=mc) - fig, axlist = mpf.plot(df, type='candle', volume=True, style=style, returnfig=True) - volume_ax = axlist[2] # panel holding the volume bars - colors = [tuple(np.round(patch.get_facecolor(), 3)) for patch in volume_ax.patches] - plt.close(fig) - return colors - - -def test_volume_color_modes_differ(): - df = _sample_df() - open_vs_close = _volume_bar_colors(df, volume_color_updown_dependancy='Open vs Close') - previous_vol = _volume_bar_colors(df, volume_color_updown_dependancy='Previous Volume') - - # The whole point of the feature: coloring by volume differs from the default. - assert previous_vol != open_vs_close - assert len(previous_vol) == len(df) - - -def test_case_and_space_insensitive(): - df = _sample_df() - canonical = _volume_bar_colors(df, volume_color_updown_dependancy='Previous Volume') - for variant in ('previous volume', 'PREVIOUSVOLUME', ' Previous Volume '): - assert _volume_bar_colors(df, volume_color_updown_dependancy=variant) == canonical - - -def test_backward_compatible_with_vcdopcod(): - df = _sample_df() - legacy = _volume_bar_colors(df, vcdopcod=True) - explicit = _volume_bar_colors(df, volume_color_updown_dependancy='Previous Close') - assert legacy == explicit - - -def test_invalid_value_raises(): - with pytest.raises(Exception): - mpf.make_marketcolors(volume_color_updown_dependancy='not a real mode') diff --git a/tests/test_volume_color_updown_dependency.py b/tests/test_volume_color_updown_dependency.py new file mode 100644 index 00000000..7efab7cf --- /dev/null +++ b/tests/test_volume_color_updown_dependency.py @@ -0,0 +1,115 @@ +""" +Regression tests for the `volume_color_updown_dependency` setting, which controls +how the up/down color of each volume bar is decided: + + "Open vs Close" (default) : today's open vs today's close (same as candles) + "Previous Close" : today's close vs the previous close + "Previous Volume" : today's volume vs the previous volume + +Value matching is case- and space-insensitive. The setting can be provided via +`make_marketcolors()`, `make_mpf_style()`, or directly as a `plot()` kwarg, and it +is backward compatible with the legacy `vcdopcod` setting. +""" + +import numpy as np +import pandas as pd +import mplfinance as mpf +import matplotlib.pyplot as plt +import pytest + +print('mpf.__version__ =', mpf.__version__) # for the record + + +def _sample_df(): + idx = pd.date_range('2024-01-01', periods=8, freq='D') + close = np.array([10, 11, 10.5, 12, 11.8, 13, 12.5, 14.0]) + # Volume deliberately rises/falls independently of price, so that the + # 'Previous Volume' coloring is distinguishable from the price-based modes. + vol = np.array([100, 80, 120, 90, 150, 60, 200, 50.0]) + df = pd.DataFrame({'Open': close - 0.2, 'High': close + 0.5, + 'Low': close - 0.5, 'Close': close, 'Volume': vol}, index=idx) + df.index.name = 'Date' + return df + + +def _bar_colors(fig, axlist): + volume_ax = axlist[2] # panel holding the volume bars + colors = [tuple(np.round(patch.get_facecolor(), 3)) for patch in volume_ax.patches] + plt.close(fig) + return colors + + +def _base_marketcolors(**mc_kwargs): + return mpf.make_marketcolors(up='g', down='r', volume={'up': 'g', 'down': 'r'}, **mc_kwargs) + + +def _colors_via_marketcolors(df, **mc_kwargs): + style = mpf.make_mpf_style(marketcolors=_base_marketcolors(**mc_kwargs)) + return _bar_colors(*mpf.plot(df, type='candle', volume=True, style=style, returnfig=True)) + + +def _colors_via_plot_kwarg(df, mode): + style = mpf.make_mpf_style(marketcolors=_base_marketcolors()) + return _bar_colors(*mpf.plot(df, type='candle', volume=True, style=style, + volume_color_updown_dependency=mode, returnfig=True)) + + +def _colors_via_make_mpf_style(df, mode): + style = mpf.make_mpf_style(marketcolors=_base_marketcolors(), + volume_color_updown_dependency=mode) + return _bar_colors(*mpf.plot(df, type='candle', volume=True, style=style, returnfig=True)) + + +def test_volume_color_modes_differ(): + df = _sample_df() + open_vs_close = _colors_via_marketcolors(df, volume_color_updown_dependency='Open vs Close') + previous_vol = _colors_via_marketcolors(df, volume_color_updown_dependency='Previous Volume') + assert previous_vol != open_vs_close + assert len(previous_vol) == len(df) + + +def test_case_and_space_insensitive(): + df = _sample_df() + canonical = _colors_via_marketcolors(df, volume_color_updown_dependency='Previous Volume') + for variant in ('previous volume', 'PREVIOUSVOLUME', ' Previous Volume '): + assert _colors_via_marketcolors(df, volume_color_updown_dependency=variant) == canonical + + +def test_backward_compatible_with_vcdopcod(): + df = _sample_df() + legacy = _colors_via_marketcolors(df, vcdopcod=True) + explicit = _colors_via_marketcolors(df, volume_color_updown_dependency='Previous Close') + assert legacy == explicit + + +def test_settable_via_plot_kwarg(): + df = _sample_df() + via_plot = _colors_via_plot_kwarg(df, 'Previous Volume') + via_marketcolors = _colors_via_marketcolors(df, volume_color_updown_dependency='Previous Volume') + assert via_plot == via_marketcolors + + +def test_settable_via_make_mpf_style(): + df = _sample_df() + via_style = _colors_via_make_mpf_style(df, 'Previous Volume') + via_marketcolors = _colors_via_marketcolors(df, volume_color_updown_dependency='Previous Volume') + assert via_style == via_marketcolors + + +def test_plot_kwarg_overrides_style_setting(): + df = _sample_df() + # Style says 'Open vs Close', but the plot() kwarg should take precedence. + style = mpf.make_mpf_style(marketcolors=_base_marketcolors(), + volume_color_updown_dependency='Open vs Close') + overridden = _bar_colors(*mpf.plot(df, type='candle', volume=True, style=style, + volume_color_updown_dependency='Previous Volume', + returnfig=True)) + pure_previous_vol = _colors_via_marketcolors(df, volume_color_updown_dependency='Previous Volume') + assert overridden == pure_previous_vol + + +def test_invalid_value_raises(): + for maker in (lambda v: mpf.make_marketcolors(volume_color_updown_dependency=v), + lambda v: mpf.make_mpf_style(volume_color_updown_dependency=v)): + with pytest.raises(Exception): + maker('not a real mode')