Skip to content

Commit e3d7fdc

Browse files
feat(altair): implement candlestick-volume (#3083)
## Implementation: `candlestick-volume` - altair Implements the **altair** version of `candlestick-volume`. **File:** `plots/candlestick-volume/implementations/altair.py` **Parent Issue:** #3068 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20620313967)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent b14ad88 commit e3d7fdc

2 files changed

Lines changed: 146 additions & 0 deletions

File tree

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
""" pyplots.ai
2+
candlestick-volume: Stock Candlestick Chart with Volume
3+
Library: altair 6.0.0 | Python 3.13.11
4+
Quality: 91/100 | Created: 2025-12-31
5+
"""
6+
7+
import altair as alt
8+
import numpy as np
9+
import pandas as pd
10+
11+
12+
# Data: Generate 60 days of realistic stock OHLC data with volume
13+
np.random.seed(42)
14+
n_days = 60
15+
dates = pd.date_range("2024-01-02", periods=n_days, freq="B") # Business days
16+
17+
# Generate price data with realistic movements
18+
price = 150.0 # Starting price
19+
opens, highs, lows, closes, volumes = [], [], [], [], []
20+
21+
for _ in range(n_days):
22+
# Daily return with slight upward drift
23+
daily_return = np.random.normal(0.001, 0.02)
24+
volatility = np.random.uniform(0.01, 0.03)
25+
26+
open_price = price
27+
close_price = price * (1 + daily_return)
28+
29+
# High and low based on volatility
30+
intraday_high = max(open_price, close_price) * (1 + np.random.uniform(0, volatility))
31+
intraday_low = min(open_price, close_price) * (1 - np.random.uniform(0, volatility))
32+
33+
opens.append(round(open_price, 2))
34+
highs.append(round(intraday_high, 2))
35+
lows.append(round(intraday_low, 2))
36+
closes.append(round(close_price, 2))
37+
38+
# Volume with some variation (higher on volatile days)
39+
base_volume = 5000000
40+
vol_multiplier = 1 + abs(daily_return) * 20
41+
volumes.append(int(base_volume * vol_multiplier * np.random.uniform(0.7, 1.3)))
42+
43+
price = close_price
44+
45+
df = pd.DataFrame({"date": dates, "open": opens, "high": highs, "low": lows, "close": closes, "volume": volumes})
46+
47+
# Determine if day is bullish (close >= open) or bearish
48+
df["direction"] = np.where(df["close"] >= df["open"], "bullish", "bearish")
49+
50+
# Color scheme for candlesticks and volume
51+
color_scale = alt.Scale(domain=["bullish", "bearish"], range=["#306998", "#FFD43B"])
52+
53+
# Base chart with shared x-axis selection
54+
base = alt.Chart(df).encode(x=alt.X("date:T", axis=alt.Axis(title="Date", labelFontSize=16, titleFontSize=20)))
55+
56+
# Candlestick wicks (high-low lines)
57+
wicks = base.mark_rule(strokeWidth=2).encode(
58+
y=alt.Y("low:Q", scale=alt.Scale(zero=False), axis=alt.Axis(title="Price ($)", labelFontSize=16, titleFontSize=20)),
59+
y2="high:Q",
60+
color=alt.Color("direction:N", scale=color_scale, legend=None),
61+
)
62+
63+
# Candlestick bodies (open-close bars)
64+
bodies = base.mark_bar(size=12).encode(
65+
y=alt.Y("open:Q", scale=alt.Scale(zero=False)),
66+
y2="close:Q",
67+
color=alt.Color("direction:N", scale=color_scale, legend=None),
68+
tooltip=[
69+
alt.Tooltip("date:T", title="Date"),
70+
alt.Tooltip("open:Q", title="Open", format="$.2f"),
71+
alt.Tooltip("high:Q", title="High", format="$.2f"),
72+
alt.Tooltip("low:Q", title="Low", format="$.2f"),
73+
alt.Tooltip("close:Q", title="Close", format="$.2f"),
74+
alt.Tooltip("volume:Q", title="Volume", format=","),
75+
],
76+
)
77+
78+
# Combine wicks and bodies for candlestick chart
79+
candlestick = (wicks + bodies).properties(width=1600, height=600, title="")
80+
81+
# Volume chart
82+
volume = (
83+
alt.Chart(df)
84+
.mark_bar(size=12)
85+
.encode(
86+
x=alt.X("date:T", axis=alt.Axis(title="Date", labelFontSize=16, titleFontSize=20)),
87+
y=alt.Y("volume:Q", axis=alt.Axis(title="Volume", labelFontSize=16, titleFontSize=20, format="~s")),
88+
color=alt.Color("direction:N", scale=color_scale, legend=None),
89+
tooltip=[alt.Tooltip("date:T", title="Date"), alt.Tooltip("volume:Q", title="Volume", format=",")],
90+
)
91+
.properties(width=1600, height=200)
92+
)
93+
94+
# Add legend separately
95+
legend_data = pd.DataFrame({"direction": ["bullish", "bearish"], "label": ["Bullish (Up)", "Bearish (Down)"]})
96+
legend = (
97+
alt.Chart(legend_data)
98+
.mark_point(size=300)
99+
.encode(y=alt.Y("label:N", axis=None), color=alt.Color("direction:N", scale=color_scale, legend=None))
100+
.properties(width=50, title="")
101+
)
102+
103+
# Combine candlestick and volume vertically
104+
combined = alt.vconcat(candlestick, volume, spacing=10).properties(
105+
title=alt.Title("candlestick-volume · altair · pyplots.ai", fontSize=28, anchor="middle")
106+
)
107+
108+
# Configure chart with appropriate styling
109+
chart = combined.configure_axis(labelFontSize=16, titleFontSize=20).configure_view(strokeWidth=0)
110+
111+
# Save as PNG (scale_factor=3 for 4800x2700)
112+
chart.save("plot.png", scale_factor=3.0)
113+
114+
# Save interactive HTML version
115+
chart_interactive = combined.configure_axis(labelFontSize=16, titleFontSize=20).configure_view(strokeWidth=0)
116+
chart_interactive.save("plot.html")
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
library: altair
2+
specification_id: candlestick-volume
3+
created: '2025-12-31T13:51:59Z'
4+
updated: '2025-12-31T13:55:34Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20620313967
7+
issue: 3068
8+
python_version: 3.13.11
9+
library_version: 6.0.0
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/candlestick-volume/altair/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/candlestick-volume/altair/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/candlestick-volume/altair/plot.html
13+
quality_score: 91
14+
review:
15+
strengths:
16+
- Excellent dual-pane layout with proper 75/25 vertical split between price and
17+
volume
18+
- Clean candlestick implementation using layered wicks (mark_rule) and bodies (mark_bar)
19+
- Colorblind-safe blue/yellow color scheme consistently applied across both panes
20+
- Comprehensive tooltips showing all OHLC values and volume
21+
- Realistic 60-day stock data generation with appropriate volatility and volume
22+
correlation
23+
- Proper title formatting per pyplots.ai conventions
24+
weaknesses:
25+
- Missing grid lines - spec explicitly requests subtle grid lines aligned across
26+
both panes
27+
- Legend defined in code but not included in the final combined chart
28+
- Missing crosshair/cursor that spans both panes as specified in requirements
29+
- No .interactive() call - misses Altair key interactivity strength for financial
30+
charts

0 commit comments

Comments
 (0)