Skip to content

Commit 12b6a3a

Browse files
feat(matplotlib): implement candlestick-volume (#3085)
## Implementation: `candlestick-volume` - matplotlib Implements the **matplotlib** version of `candlestick-volume`. **File:** `plots/candlestick-volume/implementations/matplotlib.py` **Parent Issue:** #3068 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20620311058)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 8f1bd97 commit 12b6a3a

2 files changed

Lines changed: 135 additions & 0 deletions

File tree

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
""" pyplots.ai
2+
candlestick-volume: Stock Candlestick Chart with Volume
3+
Library: matplotlib 3.10.8 | Python 3.13.11
4+
Quality: 92/100 | Created: 2025-12-31
5+
"""
6+
7+
import matplotlib.dates as mdates
8+
import matplotlib.pyplot as plt
9+
import numpy as np
10+
import pandas as pd
11+
from matplotlib.patches import Patch
12+
13+
14+
# Data - Generate realistic 60 trading days of OHLC data with volume
15+
np.random.seed(42)
16+
n_days = 60
17+
dates = pd.date_range("2024-01-02", periods=n_days, freq="B") # Business days
18+
19+
# Generate price path with realistic movement
20+
base_price = 150.0
21+
returns = np.random.normal(0.001, 0.02, n_days)
22+
prices = base_price * np.cumprod(1 + returns)
23+
24+
# Create OHLC data
25+
opens = np.zeros(n_days)
26+
highs = np.zeros(n_days)
27+
lows = np.zeros(n_days)
28+
closes = np.zeros(n_days)
29+
30+
opens[0] = base_price
31+
closes[0] = prices[0]
32+
for i in range(1, n_days):
33+
opens[i] = closes[i - 1] * (1 + np.random.normal(0, 0.005))
34+
closes[i] = prices[i]
35+
36+
# High/low based on open/close with some variation
37+
for i in range(n_days):
38+
oc_max = max(opens[i], closes[i])
39+
oc_min = min(opens[i], closes[i])
40+
highs[i] = oc_max + np.random.uniform(0.5, 2.0)
41+
lows[i] = oc_min - np.random.uniform(0.5, 2.0)
42+
43+
# Volume with higher volume on big moves
44+
base_volume = 5_000_000
45+
volume_multiplier = 1 + np.abs(closes - opens) / opens * 20
46+
volumes = base_volume * volume_multiplier * np.random.uniform(0.7, 1.3, n_days)
47+
volumes = volumes.astype(int)
48+
49+
# Colors for up/down days
50+
up_color = "#306998" # Python Blue for up
51+
down_color = "#FFD43B" # Python Yellow for down
52+
is_up = closes >= opens
53+
54+
# Create figure with two subplots sharing x-axis (75% price, 25% volume)
55+
fig, (ax_price, ax_volume) = plt.subplots(2, 1, figsize=(16, 9), gridspec_kw={"height_ratios": [3, 1]}, sharex=True)
56+
57+
# Candlestick chart - Price pane
58+
candle_width = 0.6
59+
for i in range(n_days):
60+
color = up_color if is_up[i] else down_color
61+
# Draw wick (high-low line)
62+
ax_price.plot([dates[i], dates[i]], [lows[i], highs[i]], color=color, linewidth=1.5, solid_capstyle="round")
63+
# Draw body (open-close rectangle)
64+
body_bottom = min(opens[i], closes[i])
65+
body_height = abs(closes[i] - opens[i])
66+
ax_price.bar(
67+
dates[i], body_height, width=candle_width, bottom=body_bottom, color=color, edgecolor=color, linewidth=0.5
68+
)
69+
70+
# Volume bars with matching colors
71+
for i in range(n_days):
72+
color = up_color if is_up[i] else down_color
73+
ax_volume.bar(dates[i], volumes[i], width=candle_width, color=color, alpha=0.8)
74+
75+
# Price pane styling
76+
ax_price.set_ylabel("Price ($)", fontsize=20)
77+
ax_price.tick_params(axis="both", labelsize=16)
78+
ax_price.grid(True, alpha=0.3, linestyle="--")
79+
ax_price.set_title("candlestick-volume · matplotlib · pyplots.ai", fontsize=24, pad=15)
80+
81+
# Add legend
82+
legend_elements = [
83+
Patch(facecolor=up_color, label="Up (Close ≥ Open)"),
84+
Patch(facecolor=down_color, label="Down (Close < Open)"),
85+
]
86+
ax_price.legend(handles=legend_elements, loc="upper left", fontsize=14)
87+
88+
# Volume pane styling
89+
ax_volume.set_xlabel("Date", fontsize=20)
90+
ax_volume.set_ylabel("Volume", fontsize=20)
91+
ax_volume.tick_params(axis="both", labelsize=16)
92+
ax_volume.grid(True, alpha=0.3, linestyle="--")
93+
94+
# Format y-axis for volume (in millions)
95+
ax_volume.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: f"{x / 1e6:.1f}M"))
96+
97+
# Format x-axis dates
98+
ax_volume.xaxis.set_major_locator(mdates.WeekdayLocator(byweekday=mdates.MO, interval=2))
99+
ax_volume.xaxis.set_major_formatter(mdates.DateFormatter("%b %d"))
100+
plt.setp(ax_volume.xaxis.get_majorticklabels(), rotation=45, ha="right")
101+
102+
# Ensure y-axis starts at 0 for volume
103+
ax_volume.set_ylim(bottom=0)
104+
105+
# Add crosshair cursor via spines styling (visual alignment between panes)
106+
for ax in [ax_price, ax_volume]:
107+
ax.spines["top"].set_visible(False)
108+
ax.spines["right"].set_visible(False)
109+
110+
plt.tight_layout()
111+
plt.savefig("plot.png", dpi=300, bbox_inches="tight")
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
library: matplotlib
2+
specification_id: candlestick-volume
3+
created: '2025-12-31T13:52:11Z'
4+
updated: '2025-12-31T13:56:00Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20620311058
7+
issue: 3068
8+
python_version: 3.13.11
9+
library_version: 3.10.8
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/candlestick-volume/matplotlib/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/candlestick-volume/matplotlib/plot_thumb.png
12+
preview_html: null
13+
quality_score: 92
14+
review:
15+
strengths:
16+
- Clean dual-pane layout with proper 75%/25% height ratio
17+
- Excellent color scheme using Python blue/yellow that is colorblind-safe
18+
- Volume bars correctly colored to match candlestick direction for visual consistency
19+
- Proper date formatting with business days and rotated labels
20+
- Good use of matplotlib subplots with shared x-axis
21+
weaknesses:
22+
- Missing crosshair/cursor spanning both panes as specified in requirements
23+
- Candlestick wicks are thin (linewidth=1.5) - could be slightly thicker for better
24+
visibility at scale

0 commit comments

Comments
 (0)