|
| 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") |
0 commit comments