{"spec_id":"candlestick-volume","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\ncandlestick-volume: Stock Candlestick Chart with Volume\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 92/100 | Updated: 2026-05-16\n\"\"\"\n\nimport os\nimport time\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nfrom bokeh.io import output_file, save\nfrom bokeh.layouts import column\nfrom bokeh.models import ColumnDataSource, CrosshairTool, HoverTool, NumeralTickFormatter\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme tokens\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# Okabe-Ito palette for up/down\nCOLOR_UP = \"#009E73\"  # Green (Okabe-Ito position 1)\nCOLOR_DOWN = \"#AE3030\"  # imprint red — down days\n\n# Data - Generate 60 trading days of OHLC with volume\nnp.random.seed(42)\nn_days = 60\n\n# Generate realistic stock price data\ndates = pd.date_range(start=\"2024-06-01\", periods=n_days, freq=\"B\")  # Business days\ndates_str = [d.strftime(\"%Y-%m-%d\") for d in dates]\n\n# Start price and random walk\nstart_price = 150.0\nreturns = np.random.normal(0.001, 0.02, n_days)\nclose_prices = start_price * np.cumprod(1 + returns)\n\n# Generate OHLC from close prices\nopen_prices = np.roll(close_prices, 1)\nopen_prices[0] = start_price\nhigh_prices = np.maximum(open_prices, close_prices) * (1 + np.abs(np.random.normal(0, 0.01, n_days)))\nlow_prices = np.minimum(open_prices, close_prices) * (1 - np.abs(np.random.normal(0, 0.01, n_days)))\n\n# Volume - higher on big price moves\nbase_volume = 2_000_000\nprice_change = np.abs(close_prices - open_prices) / open_prices\nvolume = base_volume * (1 + price_change * 20) * np.random.uniform(0.7, 1.3, n_days)\nvolume = volume.astype(int)\n\n# Determine up/down days\nis_up = close_prices >= open_prices\ncolors = [COLOR_UP if up else COLOR_DOWN for up in is_up]\n\n# Create DataFrame\ndf = pd.DataFrame(\n    {\n        \"date\": dates_str,\n        \"open\": open_prices,\n        \"high\": high_prices,\n        \"low\": low_prices,\n        \"close\": close_prices,\n        \"volume\": volume,\n        \"color\": colors,\n        \"is_up\": is_up,\n    }\n)\n\n# Create ColumnDataSource\nsource = ColumnDataSource(df)\n\n# Create candlestick chart (top pane - 70% height)\np_candle = figure(\n    width=4800,\n    height=1890,  # 70% of 2700\n    x_range=dates_str,\n    title=\"candlestick-volume · bokeh · anyplot.ai\",\n    tools=\"pan,wheel_zoom,box_zoom,reset,save\",\n)\n\n# Add candlestick wicks (high-low lines)\np_candle.segment(x0=\"date\", y0=\"high\", x1=\"date\", y1=\"low\", source=source, line_color=\"color\", line_width=2)\n\n# Add candlestick bodies (rectangles)\ncandle_width = 0.6\np_candle.vbar(\n    x=\"date\",\n    top=\"close\",\n    bottom=\"open\",\n    width=candle_width,\n    source=source,\n    fill_color=\"color\",\n    line_color=\"color\",\n    line_width=1,\n)\n\n# Add hover tool to candlestick\nhover_candle = HoverTool(\n    tooltips=[\n        (\"Date\", \"@date\"),\n        (\"Open\", \"$@open{0.00}\"),\n        (\"High\", \"$@high{0.00}\"),\n        (\"Low\", \"$@low{0.00}\"),\n        (\"Close\", \"$@close{0.00}\"),\n    ]\n)\np_candle.add_tools(hover_candle)\n\n# Style candlestick chart - theme-adaptive\np_candle.background_fill_color = PAGE_BG\np_candle.border_fill_color = PAGE_BG\np_candle.outline_line_color = INK_SOFT\n\np_candle.title.text_font_size = \"28pt\"\np_candle.title.text_color = INK\n\np_candle.yaxis.axis_label = \"Price ($)\"\np_candle.yaxis.axis_label_text_font_size = \"22pt\"\np_candle.yaxis.axis_label_text_color = INK\np_candle.yaxis.major_label_text_font_size = \"18pt\"\np_candle.yaxis.major_label_text_color = INK_SOFT\np_candle.yaxis.axis_line_color = INK_SOFT\np_candle.yaxis.major_tick_line_color = INK_SOFT\n\np_candle.xaxis.major_label_text_font_size = \"18pt\"\np_candle.xaxis.major_label_text_color = INK_SOFT\np_candle.xaxis.axis_line_color = INK_SOFT\np_candle.xaxis.major_tick_line_color = INK_SOFT\np_candle.xaxis.major_label_orientation = 0.8\np_candle.xaxis.visible = False  # Hide x-axis on top chart (shared with volume)\n\np_candle.xgrid.grid_line_color = INK\np_candle.ygrid.grid_line_color = INK\np_candle.xgrid.grid_line_alpha = 0.10\np_candle.ygrid.grid_line_alpha = 0.10\n\np_candle.min_border_left = 120\np_candle.min_border_right = 60\n\n# Create volume chart (bottom pane - 30% height)\np_volume = figure(\n    width=4800,\n    height=810,  # 30% of 2700\n    x_range=p_candle.x_range,  # Share x-range with candlestick\n    tools=\"pan,wheel_zoom,box_zoom,reset,save\",\n)\n\n# Add volume bars\np_volume.vbar(\n    x=\"date\",\n    top=\"volume\",\n    width=candle_width,\n    source=source,\n    fill_color=\"color\",\n    line_color=\"color\",\n    fill_alpha=1.0,\n    line_width=1,\n)\n\n# Add hover tool to volume\nhover_volume = HoverTool(tooltips=[(\"Date\", \"@date\"), (\"Volume\", \"@volume{0,0}\")])\np_volume.add_tools(hover_volume)\n\n# Style volume chart - theme-adaptive\np_volume.background_fill_color = PAGE_BG\np_volume.border_fill_color = PAGE_BG\np_volume.outline_line_color = INK_SOFT\n\np_volume.yaxis.axis_label = \"Volume\"\np_volume.yaxis.axis_label_text_font_size = \"22pt\"\np_volume.yaxis.axis_label_text_color = INK\np_volume.yaxis.major_label_text_font_size = \"18pt\"\np_volume.yaxis.major_label_text_color = INK_SOFT\np_volume.yaxis.axis_line_color = INK_SOFT\np_volume.yaxis.major_tick_line_color = INK_SOFT\n\np_volume.xaxis.axis_label = \"Date\"\np_volume.xaxis.axis_label_text_font_size = \"22pt\"\np_volume.xaxis.axis_label_text_color = INK\np_volume.xaxis.major_label_text_font_size = \"18pt\"\np_volume.xaxis.major_label_text_color = INK_SOFT\np_volume.xaxis.axis_line_color = INK_SOFT\np_volume.xaxis.major_tick_line_color = INK_SOFT\np_volume.xaxis.major_label_orientation = 0.8\n\np_volume.xgrid.grid_line_color = INK\np_volume.ygrid.grid_line_color = INK\np_volume.xgrid.grid_line_alpha = 0.10\np_volume.ygrid.grid_line_alpha = 0.10\n\np_volume.min_border_left = 120\np_volume.min_border_right = 60\n\n# Format volume y-axis to show millions\np_volume.yaxis.formatter = NumeralTickFormatter(format=\"0.0a\")\n\n# Add linked crosshair tool to both charts\ncrosshair_candle = CrosshairTool(dimensions=\"both\", line_color=INK_SOFT, line_alpha=0.5, line_width=2)\ncrosshair_volume = CrosshairTool(dimensions=\"both\", line_color=INK_SOFT, line_alpha=0.5, line_width=2)\np_candle.add_tools(crosshair_candle)\np_volume.add_tools(crosshair_volume)\n\n# Combine charts in vertical layout\nlayout = column(p_candle, p_volume)\n\n# Save as HTML\noutput_file(f\"plot-{THEME}.html\")\nsave(layout)\n\n# Screenshot with headless Chrome using Selenium\nW, H = 4800, 2700\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={W},{H}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\ndriver.set_window_size(W, H)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)  # let bokeh's JS render the canvas\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}