|
| 1 | +""" pyplots.ai |
| 2 | +candlestick-volume: Stock Candlestick Chart with Volume |
| 3 | +Library: plotly 6.5.0 | Python 3.13.11 |
| 4 | +Quality: 92/100 | Created: 2025-12-31 |
| 5 | +""" |
| 6 | + |
| 7 | +import numpy as np |
| 8 | +import pandas as pd |
| 9 | +import plotly.graph_objects as go |
| 10 | +from plotly.subplots import make_subplots |
| 11 | + |
| 12 | + |
| 13 | +# Data - Generate 60 trading days of realistic OHLC + volume data |
| 14 | +np.random.seed(42) |
| 15 | +n_days = 60 |
| 16 | +dates = pd.date_range("2024-01-02", periods=n_days, freq="B") # Business days |
| 17 | + |
| 18 | +# Start price and generate random walk |
| 19 | +start_price = 150.0 |
| 20 | +returns = np.random.normal(0.0005, 0.02, n_days) |
| 21 | +prices = start_price * np.cumprod(1 + returns) |
| 22 | + |
| 23 | +# Generate OHLC from close prices |
| 24 | +close = prices |
| 25 | +open_prices = np.roll(close, 1) |
| 26 | +open_prices[0] = start_price |
| 27 | + |
| 28 | +# Generate high/low with realistic intraday range |
| 29 | +daily_range = np.abs(np.random.normal(0.015, 0.008, n_days)) * close |
| 30 | +high = np.maximum(open_prices, close) + daily_range * np.random.uniform(0.3, 0.7, n_days) |
| 31 | +low = np.minimum(open_prices, close) - daily_range * np.random.uniform(0.3, 0.7, n_days) |
| 32 | + |
| 33 | +# Volume with correlation to price movement (higher volume on bigger moves) |
| 34 | +base_volume = 5_000_000 |
| 35 | +price_change = np.abs(close - open_prices) / open_prices |
| 36 | +volume = base_volume * (1 + 3 * price_change) * np.random.uniform(0.7, 1.3, n_days) |
| 37 | +volume = volume.astype(int) |
| 38 | + |
| 39 | +# Determine up/down days for coloring |
| 40 | +is_up = close >= open_prices |
| 41 | +colors = ["#306998" if up else "#FFD43B" for up in is_up] |
| 42 | + |
| 43 | +# Create subplot with shared x-axis |
| 44 | +# Price pane: 75% height, Volume pane: 25% height |
| 45 | +fig = make_subplots(rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.03, row_heights=[0.75, 0.25]) |
| 46 | + |
| 47 | +# Add candlestick chart |
| 48 | +fig.add_trace( |
| 49 | + go.Candlestick( |
| 50 | + x=dates, |
| 51 | + open=open_prices, |
| 52 | + high=high, |
| 53 | + low=low, |
| 54 | + close=close, |
| 55 | + increasing={"line": {"color": "#306998", "width": 2}, "fillcolor": "#306998"}, |
| 56 | + decreasing={"line": {"color": "#FFD43B", "width": 2}, "fillcolor": "#FFD43B"}, |
| 57 | + name="Price", |
| 58 | + showlegend=False, |
| 59 | + ), |
| 60 | + row=1, |
| 61 | + col=1, |
| 62 | +) |
| 63 | + |
| 64 | +# Add volume bars |
| 65 | +fig.add_trace( |
| 66 | + go.Bar(x=dates, y=volume, marker={"color": colors, "line": {"width": 0}}, name="Volume", showlegend=False), |
| 67 | + row=2, |
| 68 | + col=1, |
| 69 | +) |
| 70 | + |
| 71 | +# Update layout for professional appearance |
| 72 | +fig.update_layout( |
| 73 | + title={"text": "candlestick-volume · plotly · pyplots.ai", "font": {"size": 32}, "x": 0.5, "xanchor": "center"}, |
| 74 | + template="plotly_white", |
| 75 | + hovermode="x unified", |
| 76 | + # Add crosshair cursor |
| 77 | + xaxis={ |
| 78 | + "showspikes": True, |
| 79 | + "spikemode": "across", |
| 80 | + "spikesnap": "cursor", |
| 81 | + "spikecolor": "#888888", |
| 82 | + "spikethickness": 1, |
| 83 | + "spikedash": "solid", |
| 84 | + }, |
| 85 | + xaxis2={ |
| 86 | + "showspikes": True, |
| 87 | + "spikemode": "across", |
| 88 | + "spikesnap": "cursor", |
| 89 | + "spikecolor": "#888888", |
| 90 | + "spikethickness": 1, |
| 91 | + "spikedash": "solid", |
| 92 | + }, |
| 93 | + yaxis={ |
| 94 | + "showspikes": True, |
| 95 | + "spikemode": "across", |
| 96 | + "spikesnap": "cursor", |
| 97 | + "spikecolor": "#888888", |
| 98 | + "spikethickness": 1, |
| 99 | + "spikedash": "solid", |
| 100 | + }, |
| 101 | + yaxis2={ |
| 102 | + "showspikes": True, |
| 103 | + "spikemode": "across", |
| 104 | + "spikesnap": "cursor", |
| 105 | + "spikecolor": "#888888", |
| 106 | + "spikethickness": 1, |
| 107 | + "spikedash": "solid", |
| 108 | + }, |
| 109 | + # Disable range slider |
| 110 | + xaxis_rangeslider_visible=False, |
| 111 | + # Margins for proper spacing |
| 112 | + margin={"l": 80, "r": 40, "t": 100, "b": 60}, |
| 113 | +) |
| 114 | + |
| 115 | +# Update axes styling |
| 116 | +fig.update_xaxes( |
| 117 | + title={"text": "Date", "font": {"size": 22}}, |
| 118 | + tickfont={"size": 18}, |
| 119 | + gridcolor="rgba(128,128,128,0.2)", |
| 120 | + gridwidth=1, |
| 121 | + row=2, |
| 122 | + col=1, |
| 123 | +) |
| 124 | + |
| 125 | +fig.update_yaxes( |
| 126 | + title={"text": "Price ($)", "font": {"size": 22}}, |
| 127 | + tickfont={"size": 18}, |
| 128 | + tickformat="$.0f", |
| 129 | + gridcolor="rgba(128,128,128,0.2)", |
| 130 | + gridwidth=1, |
| 131 | + row=1, |
| 132 | + col=1, |
| 133 | +) |
| 134 | + |
| 135 | +fig.update_yaxes( |
| 136 | + title={"text": "Volume", "font": {"size": 22}}, |
| 137 | + tickfont={"size": 18}, |
| 138 | + tickformat=".2s", |
| 139 | + gridcolor="rgba(128,128,128,0.2)", |
| 140 | + gridwidth=1, |
| 141 | + row=2, |
| 142 | + col=1, |
| 143 | +) |
| 144 | + |
| 145 | +# Hide x-axis title on price pane |
| 146 | +fig.update_xaxes(title=None, row=1, col=1) |
| 147 | + |
| 148 | +# Save as PNG (4800 x 2700 pixels) |
| 149 | +fig.write_image("plot.png", width=1600, height=900, scale=3) |
| 150 | + |
| 151 | +# Save as HTML for interactivity |
| 152 | +fig.write_html("plot.html", include_plotlyjs="cdn") |
0 commit comments