{"spec_id":"candlestick-volume","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\ncandlestick-volume: Stock Candlestick Chart with Volume\nLibrary: plotly 6.7.0 | Python 3.13.13\nQuality: 88/100 | Updated: 2026-05-16\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nimport plotly.graph_objects as go\nfrom plotly.subplots import make_subplots\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nGRID = \"rgba(26,26,23,0.10)\" if THEME == \"light\" else \"rgba(240,239,232,0.10)\"\n\nCOLOR_UP = \"#009E73\"  # Okabe-Ito position 1 (brand green)\nCOLOR_DOWN = \"#AE3030\"  # imprint red — down days\n\n# Data - Generate 60 trading days of realistic OHLC + volume data\nnp.random.seed(42)\nn_days = 60\ndates = pd.date_range(\"2024-01-02\", periods=n_days, freq=\"B\")  # Business days\n\n# Start price and generate random walk\nstart_price = 150.0\nreturns = np.random.normal(0.0005, 0.02, n_days)\nprices = start_price * np.cumprod(1 + returns)\n\n# Generate OHLC from close prices\nclose = prices\nopen_prices = np.roll(close, 1)\nopen_prices[0] = start_price\n\n# Generate high/low with realistic intraday range\ndaily_range = np.abs(np.random.normal(0.015, 0.008, n_days)) * close\nhigh = np.maximum(open_prices, close) + daily_range * np.random.uniform(0.3, 0.7, n_days)\nlow = np.minimum(open_prices, close) - daily_range * np.random.uniform(0.3, 0.7, n_days)\n\n# Volume with correlation to price movement (higher volume on bigger moves)\nbase_volume = 5_000_000\nprice_change = np.abs(close - open_prices) / open_prices\nvolume = base_volume * (1 + 3 * price_change) * np.random.uniform(0.7, 1.3, n_days)\nvolume = volume.astype(int)\n\n# Determine up/down days for coloring\nis_up = close >= open_prices\ncolors = [COLOR_UP if up else COLOR_DOWN for up in is_up]\n\n# Create subplot with shared x-axis\n# Price pane: 75% height, Volume pane: 25% height\nfig = make_subplots(rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.03, row_heights=[0.75, 0.25])\n\n# Add candlestick chart\nfig.add_trace(\n    go.Candlestick(\n        x=dates,\n        open=open_prices,\n        high=high,\n        low=low,\n        close=close,\n        increasing={\"line\": {\"color\": COLOR_UP, \"width\": 2}, \"fillcolor\": COLOR_UP},\n        decreasing={\"line\": {\"color\": COLOR_DOWN, \"width\": 2}, \"fillcolor\": COLOR_DOWN},\n        name=\"Price\",\n        showlegend=False,\n    ),\n    row=1,\n    col=1,\n)\n\n# Add volume bars\nfig.add_trace(\n    go.Bar(x=dates, y=volume, marker={\"color\": colors, \"line\": {\"width\": 0}}, name=\"Volume\", showlegend=False),\n    row=2,\n    col=1,\n)\n\n# Update layout for professional appearance\nfig.update_layout(\n    title={\n        \"text\": \"candlestick-volume · plotly · anyplot.ai\",\n        \"font\": {\"size\": 28, \"color\": INK},\n        \"x\": 0.5,\n        \"xanchor\": \"center\",\n    },\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    font={\"color\": INK},\n    hovermode=\"x unified\",\n    # Add crosshair cursor\n    xaxis={\n        \"showspikes\": True,\n        \"spikemode\": \"across\",\n        \"spikesnap\": \"cursor\",\n        \"spikecolor\": INK_SOFT,\n        \"spikethickness\": 1,\n        \"spikedash\": \"solid\",\n    },\n    xaxis2={\n        \"showspikes\": True,\n        \"spikemode\": \"across\",\n        \"spikesnap\": \"cursor\",\n        \"spikecolor\": INK_SOFT,\n        \"spikethickness\": 1,\n        \"spikedash\": \"solid\",\n    },\n    yaxis={\n        \"showspikes\": True,\n        \"spikemode\": \"across\",\n        \"spikesnap\": \"cursor\",\n        \"spikecolor\": INK_SOFT,\n        \"spikethickness\": 1,\n        \"spikedash\": \"solid\",\n    },\n    yaxis2={\n        \"showspikes\": True,\n        \"spikemode\": \"across\",\n        \"spikesnap\": \"cursor\",\n        \"spikecolor\": INK_SOFT,\n        \"spikethickness\": 1,\n        \"spikedash\": \"solid\",\n    },\n    # Disable range slider\n    xaxis_rangeslider_visible=False,\n    # Margins for proper spacing\n    margin={\"l\": 80, \"r\": 40, \"t\": 100, \"b\": 60},\n)\n\n# Update axes styling\nfig.update_xaxes(\n    title={\"text\": \"Date\", \"font\": {\"size\": 22, \"color\": INK}},\n    tickfont={\"size\": 18, \"color\": INK_SOFT},\n    gridcolor=GRID,\n    gridwidth=1,\n    linecolor=INK_SOFT,\n    zerolinecolor=INK_SOFT,\n    row=2,\n    col=1,\n)\n\nfig.update_yaxes(\n    title={\"text\": \"Price ($)\", \"font\": {\"size\": 22, \"color\": INK}},\n    tickfont={\"size\": 18, \"color\": INK_SOFT},\n    tickformat=\"$.0f\",\n    gridcolor=GRID,\n    gridwidth=1,\n    linecolor=INK_SOFT,\n    zerolinecolor=INK_SOFT,\n    row=1,\n    col=1,\n)\n\nfig.update_yaxes(\n    title={\"text\": \"Volume\", \"font\": {\"size\": 22, \"color\": INK}},\n    tickfont={\"size\": 18, \"color\": INK_SOFT},\n    tickformat=\".2s\",\n    gridcolor=GRID,\n    gridwidth=1,\n    linecolor=INK_SOFT,\n    zerolinecolor=INK_SOFT,\n    row=2,\n    col=1,\n)\n\n# Hide x-axis title on price pane\nfig.update_xaxes(title=None, row=1, col=1)\n\n# Save as PNG and HTML\nfig.write_image(f\"plot-{THEME}.png\", width=1600, height=900, scale=3)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}