Skip to content

Commit fa51ee0

Browse files
feat(seaborn): implement candlestick-volume (#3087)
## Implementation: `candlestick-volume` - seaborn Implements the **seaborn** version of `candlestick-volume`. **File:** `plots/candlestick-volume/implementations/seaborn.py` **Parent Issue:** #3068 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20620312762)* --------- 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 9eca40f commit fa51ee0

2 files changed

Lines changed: 245 additions & 0 deletions

File tree

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
""" pyplots.ai
2+
candlestick-volume: Stock Candlestick Chart with Volume
3+
Library: seaborn 0.13.2 | Python 3.13.11
4+
Quality: 88/100 | Created: 2025-12-31
5+
"""
6+
7+
import matplotlib.pyplot as plt
8+
import numpy as np
9+
import pandas as pd
10+
import seaborn as sns
11+
from matplotlib.patches import Patch
12+
13+
14+
# Set seaborn style for consistent aesthetics
15+
sns.set_style("whitegrid")
16+
sns.set_context("talk", font_scale=1.2)
17+
18+
# Generate realistic stock data for 60 trading days
19+
np.random.seed(42)
20+
n_days = 60
21+
22+
# Start with a base price and generate realistic price movements
23+
dates = pd.date_range("2024-01-02", periods=n_days, freq="B") # Business days
24+
base_price = 150.0
25+
26+
# Generate price series with trends and volatility
27+
returns = np.random.normal(0.001, 0.02, n_days) # Daily returns
28+
prices = base_price * np.cumprod(1 + returns)
29+
30+
# Generate OHLC from the price series
31+
opens = np.zeros(n_days)
32+
highs = np.zeros(n_days)
33+
lows = np.zeros(n_days)
34+
closes = np.zeros(n_days)
35+
36+
opens[0] = base_price
37+
for i in range(n_days):
38+
if i > 0:
39+
opens[i] = closes[i - 1] + np.random.normal(0, 0.5)
40+
closes[i] = prices[i]
41+
# Ensure more significant price moves for better visibility
42+
daily_range = abs(closes[i] - opens[i]) + np.random.uniform(1.0, 3.0)
43+
highs[i] = max(opens[i], closes[i]) + np.random.uniform(0.5, daily_range * 0.6)
44+
lows[i] = min(opens[i], closes[i]) - np.random.uniform(0.5, daily_range * 0.6)
45+
46+
# Generate volume with some correlation to price movements
47+
base_volume = 5_000_000
48+
volume = base_volume + np.random.normal(0, 1_000_000, n_days)
49+
# Higher volume on bigger price moves
50+
price_change = np.abs(closes - opens)
51+
volume = volume + price_change * 500_000
52+
volume = np.clip(volume, 1_000_000, 15_000_000).astype(int)
53+
54+
# Create DataFrame
55+
df = pd.DataFrame({"date": dates, "open": opens, "high": highs, "low": lows, "close": closes, "volume": volume})
56+
57+
# Determine bullish vs bearish candles
58+
df["bullish"] = df["close"] >= df["open"]
59+
df["day_idx"] = range(len(df))
60+
61+
# Colors for consistent styling
62+
BULLISH_COLOR = "#306998" # Python Blue
63+
BEARISH_COLOR = "#FFD43B" # Python Yellow
64+
65+
# Create figure with two subplots sharing x-axis (75% price, 25% volume)
66+
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(16, 9), height_ratios=[3, 1], sharex=True, gridspec_kw={"hspace": 0.05})
67+
68+
# Set grid below chart elements FIRST before any plotting
69+
for ax in [ax1, ax2]:
70+
ax.set_axisbelow(True)
71+
72+
# === Upper pane: Candlestick chart using seaborn ===
73+
# Prepare data for seaborn lineplot (wicks) - create long format for high-low lines
74+
df["wick_min"] = df[["open", "close"]].min(axis=1)
75+
df["wick_max"] = df[["open", "close"]].max(axis=1)
76+
df["body_height"] = (df["wick_max"] - df["wick_min"]).clip(lower=0.5) # Minimum height for visibility
77+
df["direction"] = df["bullish"].map({True: "Bullish", False: "Bearish"})
78+
79+
# Draw high-low wicks using seaborn lineplot with units parameter
80+
wick_long = pd.melt(
81+
df[["day_idx", "high", "low", "direction"]],
82+
id_vars=["day_idx", "direction"],
83+
value_vars=["low", "high"],
84+
var_name="price_type",
85+
value_name="price",
86+
).sort_values(["day_idx", "price_type"])
87+
88+
sns.lineplot(
89+
data=wick_long,
90+
x="day_idx",
91+
y="price",
92+
hue="direction",
93+
palette={"Bullish": BULLISH_COLOR, "Bearish": BEARISH_COLOR},
94+
linewidth=2,
95+
units="day_idx",
96+
estimator=None,
97+
legend=False,
98+
ax=ax1,
99+
zorder=2,
100+
)
101+
102+
# Draw candle bodies with minimum height for visibility
103+
for _, row in df.iterrows():
104+
color = BULLISH_COLOR if row["bullish"] else BEARISH_COLOR
105+
body_low = row["wick_min"]
106+
body_high = body_low + row["body_height"]
107+
ax1.fill_between(
108+
[row["day_idx"] - 0.35, row["day_idx"] + 0.35],
109+
[body_low] * 2,
110+
[body_high] * 2,
111+
color=color,
112+
alpha=1.0,
113+
linewidth=0,
114+
zorder=3,
115+
)
116+
117+
# Style the price axis
118+
ax1.set_ylabel("Price ($)", fontsize=20)
119+
ax1.set_xlabel("")
120+
ax1.tick_params(axis="both", labelsize=16)
121+
ax1.set_title("candlestick-volume · seaborn · pyplots.ai", fontsize=24, pad=15)
122+
123+
# Set y-axis range with padding
124+
price_min = df["low"].min()
125+
price_max = df["high"].max()
126+
price_padding = (price_max - price_min) * 0.05
127+
ax1.set_ylim(price_min - price_padding, price_max + price_padding)
128+
129+
# === Lower pane: Volume bars ===
130+
# Draw volume bars directly with matplotlib to ensure single bar per day
131+
# Colors match the candlestick bullish/bearish scheme
132+
bar_colors = [BULLISH_COLOR if b else BEARISH_COLOR for b in df["bullish"]]
133+
ax2.bar(df["day_idx"], df["volume"], color=bar_colors, width=0.7, alpha=0.9, zorder=2)
134+
135+
# Style the volume axis with units
136+
ax2.set_ylabel("Volume (M shares)", fontsize=20)
137+
ax2.set_xlabel("Date", fontsize=20)
138+
ax2.tick_params(axis="both", labelsize=16)
139+
140+
# Format y-axis for volume (millions)
141+
ax2.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: f"{x / 1e6:.1f}M"))
142+
143+
# === Aligned grid lines across both panes ===
144+
# Use consistent grid styling (grid already set below via set_axisbelow earlier)
145+
for ax in [ax1, ax2]:
146+
ax.grid(True, axis="both", alpha=0.3, linestyle="--", linewidth=0.8)
147+
148+
# Configure x-axis with date labels at regular intervals
149+
n_ticks = 6
150+
tick_positions = np.linspace(0, len(df) - 1, n_ticks, dtype=int)
151+
tick_labels = [df.iloc[i]["date"].strftime("%b %d") for i in tick_positions]
152+
ax2.set_xticks(tick_positions)
153+
ax2.set_xticklabels(tick_labels, rotation=45, ha="right")
154+
155+
# Add legend to price pane
156+
legend_elements = [
157+
Patch(facecolor=BULLISH_COLOR, label="Bullish (Close ≥ Open)"),
158+
Patch(facecolor=BEARISH_COLOR, label="Bearish (Close < Open)"),
159+
]
160+
ax1.legend(handles=legend_elements, loc="upper left", fontsize=14, framealpha=0.9)
161+
162+
# === Add crosshair cursor spanning both panes ===
163+
# Draw static crosshair lines at a representative position to show the feature
164+
# Using a position at approximately 2/3 of the chart for good visibility
165+
crosshair_idx = int(len(df) * 0.65)
166+
crosshair_price = (df.iloc[crosshair_idx]["high"] + df.iloc[crosshair_idx]["low"]) / 2
167+
crosshair_volume = df.iloc[crosshair_idx]["volume"]
168+
169+
# Vertical crosshair line spanning both panes - more prominent styling
170+
crosshair_color = "#E63946" # Red for high visibility
171+
for ax in [ax1, ax2]:
172+
ax.axvline(x=crosshair_idx, color=crosshair_color, linestyle="--", linewidth=2, alpha=0.8, zorder=5)
173+
174+
# Horizontal crosshair lines in both panes for precise reading
175+
ax1.axhline(y=crosshair_price, color=crosshair_color, linestyle="--", linewidth=2, alpha=0.8, zorder=5)
176+
ax2.axhline(y=crosshair_volume, color=crosshair_color, linestyle="--", linewidth=2, alpha=0.8, zorder=5)
177+
178+
# Add crosshair label annotation for price pane with arrow
179+
ax1.annotate(
180+
f"${crosshair_price:.2f}",
181+
xy=(crosshair_idx, crosshair_price),
182+
xytext=(crosshair_idx + 5, crosshair_price + (price_max - price_min) * 0.08),
183+
fontsize=14,
184+
fontweight="bold",
185+
color=crosshair_color,
186+
va="center",
187+
arrowprops={"arrowstyle": "->", "color": crosshair_color, "lw": 1.5},
188+
bbox={"boxstyle": "round,pad=0.4", "facecolor": "white", "edgecolor": crosshair_color, "alpha": 0.95},
189+
)
190+
191+
# Add crosshair label annotation for volume pane with arrow
192+
ax2.annotate(
193+
f"{crosshair_volume / 1e6:.1f}M shares",
194+
xy=(crosshair_idx, crosshair_volume),
195+
xytext=(crosshair_idx + 5, crosshair_volume * 1.15),
196+
fontsize=14,
197+
fontweight="bold",
198+
color=crosshair_color,
199+
va="center",
200+
arrowprops={"arrowstyle": "->", "color": crosshair_color, "lw": 1.5},
201+
bbox={"boxstyle": "round,pad=0.4", "facecolor": "white", "edgecolor": crosshair_color, "alpha": 0.95},
202+
)
203+
204+
# Add date label at crosshair position
205+
crosshair_date = df.iloc[crosshair_idx]["date"].strftime("%b %d, %Y")
206+
ax2.annotate(
207+
crosshair_date,
208+
xy=(crosshair_idx, 0),
209+
xytext=(crosshair_idx, -ax2.get_ylim()[1] * 0.15),
210+
fontsize=12,
211+
fontweight="bold",
212+
color=crosshair_color,
213+
ha="center",
214+
va="top",
215+
bbox={"boxstyle": "round,pad=0.3", "facecolor": "white", "edgecolor": crosshair_color, "alpha": 0.95},
216+
annotation_clip=False,
217+
)
218+
219+
# Adjust layout and save
220+
fig.subplots_adjust(left=0.08, right=0.95, top=0.92, bottom=0.12, hspace=0.05)
221+
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: seaborn
2+
specification_id: candlestick-volume
3+
created: '2025-12-31T13:52:19Z'
4+
updated: '2025-12-31T14:44:44Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20620312762
7+
issue: 3068
8+
python_version: 3.13.11
9+
library_version: 0.13.2
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/candlestick-volume/seaborn/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/candlestick-volume/seaborn/plot_thumb.png
12+
preview_html: null
13+
quality_score: 88
14+
review:
15+
strengths:
16+
- Excellent dual-pane layout with proper 75/25 height ratio as specified
17+
- Good color scheme using Python blue/yellow that is colorblind accessible
18+
- Clean seaborn styling with whitegrid and talk context provides professional appearance
19+
- Realistic stock data generation with price-volume correlation
20+
- Well-formatted axis labels with units and proper tick formatting for millions
21+
weaknesses:
22+
- Crosshair feature appears in code but is not visible in the rendered output image
23+
- Legend placement in upper left may overlap with price data in some scenarios
24+
- Volume axis label shows just Volume instead of Volume (M shares) as coded

0 commit comments

Comments
 (0)