{"spec_id":"candlestick-volume","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\ncandlestick-volume: Stock Candlestick Chart with Volume\nLibrary: letsplot 4.9.0 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-05-16\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import *\n\n\nLetsPlot.setup_html()\n\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\n# Data - 60 trading days of synthetic stock data\nnp.random.seed(42)\nn_days = 60\ndates = pd.date_range(\"2024-01-02\", periods=n_days, freq=\"B\")\n\n# Generate realistic price movement with more dramatic reversals\nreturns = np.random.normal(0.001, 0.025, n_days)\nreturns[15] = -0.08  # Dramatic drop\nreturns[35] = 0.06  # Strong bounce\nreturns[45] = -0.05  # Another reversal\nclose_prices = 150 * np.cumprod(1 + returns)\n\n# Generate OHLC from close prices\nopen_prices = np.roll(close_prices, 1)\nopen_prices[0] = 150\nhigh_prices = np.maximum(open_prices, close_prices) * (1 + np.abs(np.random.normal(0, 0.012, n_days)))\nlow_prices = np.minimum(open_prices, close_prices) * (1 - np.abs(np.random.normal(0, 0.012, n_days)))\n\n# Generate volume with correlation to price movement\nbase_volume = 5_000_000\nvolatility = np.abs(close_prices - open_prices) / open_prices\nvolume = base_volume * (1 + volatility * 10 + np.random.uniform(-0.3, 0.3, n_days))\nvolume = volume.astype(int)\n\n# Determine up/down days for coloring\ndirection = [\"Up\" if c >= o else \"Down\" for c, o in zip(close_prices, open_prices)]\n\n# Create date labels for x-axis (show every 10 trading days)\ndate_labels = [d.strftime(\"%b %d\") for d in dates]\ndate_breaks = list(range(0, n_days, 10))\ndate_tick_labels = [date_labels[i] for i in date_breaks]\n\ndf = pd.DataFrame(\n    {\n        \"date\": dates,\n        \"date_idx\": range(n_days),\n        \"date_label\": date_labels,\n        \"open\": open_prices,\n        \"high\": high_prices,\n        \"low\": low_prices,\n        \"close\": close_prices,\n        \"volume\": volume,\n        \"direction\": direction,\n    }\n)\n\n# Colorblind-safe colors (first series Okabe-Ito, second is orange)\ncolor_up = \"#009E73\"\ncolor_down = \"#AE3030\"  # imprint red — down days\n\n# Create volume breaks and labels (inline formatting)\nvol_min, vol_max = df[\"volume\"].min(), df[\"volume\"].max()\nvol_breaks = [int(vol_min), int((vol_min + vol_max) / 2), int(vol_max)]\nvol_labels = [f\"{v / 1_000_000:.1f}M\" if v >= 1_000_000 else f\"{v / 1_000:.0f}K\" for v in vol_breaks]\n\n# Theme-adaptive styling\nanyplot_theme = theme(\n    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    panel_background=element_rect(fill=PAGE_BG),\n    panel_grid_major=element_line(color=INK_SOFT, size=0.25),\n    panel_grid_minor=element_blank(),\n    axis_title=element_text(color=INK, size=20),\n    axis_text=element_text(color=INK_SOFT, size=16),\n    axis_line=element_line(color=INK_SOFT, size=0.3),\n    plot_title=element_text(color=INK, size=24),\n    legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n    legend_text=element_text(color=INK_SOFT, size=16),\n    legend_title=element_text(color=INK, size=18),\n)\n\n# Create candlestick chart (main pane)\ncandle_plot = (\n    ggplot(df)\n    # Wicks (high-low lines) - thicker for visibility\n    + geom_segment(aes(x=\"date_idx\", xend=\"date_idx\", y=\"low\", yend=\"high\", color=\"direction\"), size=1.5)\n    # Bodies (open-close rectangles)\n    + geom_segment(aes(x=\"date_idx\", xend=\"date_idx\", y=\"open\", yend=\"close\", color=\"direction\"), size=6.0)\n    + scale_color_manual(values={\"Up\": color_up, \"Down\": color_down}, name=\"Direction\")\n    + scale_x_continuous(breaks=date_breaks, labels=date_tick_labels)\n    + labs(title=\"Stock Trading · candlestick-volume · letsplot · anyplot.ai\", y=\"Price ($)\", x=\"\")\n    + anyplot_theme\n    + theme(\n        axis_text_x=element_blank(),\n        legend_position=[0.5, 0.95],\n        legend_justification=[0.5, 1.0],\n        legend_direction=\"horizontal\",\n        plot_margin=[40, 20, 2, 10],\n    )\n    + ggsize(1600, 630)\n)\n\n# Volume chart (lower pane)\nvolume_plot = (\n    ggplot(df)\n    + geom_bar(aes(x=\"date_idx\", y=\"volume\", fill=\"direction\"), stat=\"identity\", width=0.8)\n    + scale_fill_manual(values={\"Up\": color_up, \"Down\": color_down}, name=\"Direction\")\n    + scale_x_continuous(breaks=date_breaks, labels=date_tick_labels)\n    + scale_y_continuous(breaks=vol_breaks, labels=vol_labels)\n    + labs(x=\"Date (2024)\", y=\"Volume (shares)\")\n    + anyplot_theme\n    + theme(legend_position=\"none\", plot_margin=[2, 20, 10, 10])\n    + ggsize(1600, 270)\n)\n\n# Use gggrid for dual-pane layout with tighter spacing\ncombined = gggrid([candle_plot, volume_plot], ncol=1, heights=[0.7, 0.3])\n\n# Save outputs\nggsave(combined, f\"plot-{THEME}.png\", scale=3, path=\".\")\nggsave(combined, f\"plot-{THEME}.html\", path=\".\")\n"}