{"spec_id":"candlestick-basic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\ncandlestick-basic: Basic Candlestick Chart\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 92/100 | Updated: 2026-05-30\n\"\"\"\n\nimport os\nimport sys\nimport time\nfrom pathlib import Path\n\n\n# Remove this file's directory from sys.path so `import bokeh` resolves\n# the installed package rather than this file (bokeh.py).\n_here = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p) != _here]\n\nimport numpy as np\nimport pandas as pd\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, HoverTool, Label, NumeralTickFormatter, Range1d, Span\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme tokens — Imprint palette, theme-adaptive chrome\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# Imprint palette — semantic exception for finance: up=green, down=red\nCOLOR_BULL = \"#009E73\"  # Imprint position 1 — bullish (gain/up)\nCOLOR_BEAR = \"#AE3030\"  # Imprint position 5 — bearish (loss/down)\n\n# Data — 30 trading days of OHLC data for ACME Corp\nnp.random.seed(42)\nn_days = 30\nstart_price = 150.0\ndates = pd.date_range(start=\"2024-01-02\", periods=n_days, freq=\"B\")\n\nreturns = np.random.randn(n_days) * 0.018\nreturns[:10] -= 0.002\nreturns[10:20] -= 0.005\nreturns[20:] += 0.008\nprices = start_price * np.cumprod(1 + returns)\n\nopen_prices = []\nhigh_prices = []\nlow_prices = []\nclose_prices = []\n\nfor i, close in enumerate(prices):\n    if i == 0:\n        open_price = start_price\n    else:\n        open_price = close_prices[-1]\n    daily_range = abs(np.random.randn()) * 0.012 * close\n    high = max(open_price, close) + daily_range\n    low = min(open_price, close) - daily_range\n    open_prices.append(open_price)\n    high_prices.append(high)\n    low_prices.append(low)\n    close_prices.append(close)\n\ndf = pd.DataFrame({\"date\": dates, \"open\": open_prices, \"high\": high_prices, \"low\": low_prices, \"close\": close_prices})\ndf[\"bullish\"] = df[\"close\"] >= df[\"open\"]\ndf[\"date_str\"] = df[\"date\"].dt.strftime(\"%b %d, %Y\")\n\nbullish_df = df[df[\"bullish\"]].copy()\nbearish_df = df[~df[\"bullish\"]].copy()\nsource_bullish = ColumnDataSource(bullish_df)\nsource_bearish = ColumnDataSource(bearish_df)\n\n# Figure — 3200×1800 landscape; reserved borders for 42pt axis labels\ntitle = \"ACME Corp Stock · candlestick-basic · python · bokeh · anyplot.ai\"\np = figure(\n    width=3200,\n    height=1800,\n    x_axis_type=\"datetime\",\n    title=title,\n    x_axis_label=\"Date\",\n    y_axis_label=\"Price (USD)\",\n    tools=\"\",\n    toolbar_location=None,\n    min_border_bottom=160,\n    min_border_left=180,\n    min_border_top=110,\n    min_border_right=50,\n)\n\n# Tighten x-range\nx_pad = pd.Timedelta(days=1)\np.x_range = Range1d(start=dates[0] - x_pad, end=dates[-1] + x_pad)\n\n# Candle width — 75% of one business day in milliseconds\ncandle_width = 0.75 * 24 * 60 * 60 * 1000\n\n# Wicks — colored to match candle bodies, visibly thinner than bodies\np.segment(x0=\"date\", y0=\"high\", x1=\"date\", y1=\"low\", source=source_bullish, color=COLOR_BULL, line_width=3)\np.segment(x0=\"date\", y0=\"high\", x1=\"date\", y1=\"low\", source=source_bearish, color=COLOR_BEAR, line_width=3)\n\n# Candle bodies\nbull_bars = p.vbar(\n    x=\"date\",\n    top=\"close\",\n    bottom=\"open\",\n    width=candle_width,\n    source=source_bullish,\n    fill_color=COLOR_BULL,\n    line_color=COLOR_BULL,\n    line_width=1,\n    legend_label=\"Bullish\",\n)\nbear_bars = p.vbar(\n    x=\"date\",\n    top=\"open\",\n    bottom=\"close\",\n    width=candle_width,\n    source=source_bearish,\n    fill_color=PAGE_BG,  # hollow body — CVD-safe: solid=bullish, hollow=bearish\n    line_color=COLOR_BEAR,\n    line_width=2,\n    legend_label=\"Bearish\",\n)\n\n# Hover tooltips — Bokeh interactive feature\nhover = HoverTool(\n    renderers=[bull_bars, bear_bars],\n    tooltips=[\n        (\"Date\", \"@date_str\"),\n        (\"Open\", \"@open{$0.00}\"),\n        (\"High\", \"@high{$0.00}\"),\n        (\"Low\", \"@low{$0.00}\"),\n        (\"Close\", \"@close{$0.00}\"),\n    ],\n    mode=\"vline\",\n)\np.add_tools(hover)\n\n# Reversal annotation — mark the Jan 22 trend-change pivot for active storytelling\nreversal_ts = int(pd.Timestamp(\"2024-01-22\").timestamp() * 1000)\np.add_layout(\n    Span(\n        location=reversal_ts, dimension=\"height\", line_color=INK_SOFT, line_dash=\"dashed\", line_width=2, line_alpha=0.6\n    )\n)\np.add_layout(\n    Label(\n        x=reversal_ts,\n        y=df[\"high\"].max() * 0.99,\n        x_units=\"data\",\n        y_units=\"data\",\n        text=\"  Trend Reversal\",\n        text_color=INK_SOFT,\n        text_font_size=\"28pt\",\n        text_baseline=\"top\",\n        text_align=\"left\",\n    )\n)\n\n# Typography — 3200×1800 sizing (title 65 chars → full 50pt)\np.title.text_font_size = \"50pt\"\np.title.text_font_style = \"normal\"\np.title.text_color = INK\np.xaxis.axis_label_text_font_size = \"42pt\"\np.yaxis.axis_label_text_font_size = \"42pt\"\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\np.xaxis.major_label_text_font_size = \"34pt\"\np.yaxis.major_label_text_font_size = \"34pt\"\np.xaxis.major_label_text_color = INK_SOFT\np.yaxis.major_label_text_color = INK_SOFT\n\n# Y-axis dollar formatting\np.yaxis.formatter = NumeralTickFormatter(format=\"$0\")\n\n# Grid — y-axis only, subtle\np.xgrid.grid_line_color = None\np.ygrid.grid_line_color = INK\np.ygrid.grid_line_alpha = 0.15\np.ygrid.grid_line_width = 1\n\n# Axis chrome — theme-adaptive, minimalist\np.outline_line_color = None\np.xaxis.axis_line_color = INK_SOFT\np.yaxis.axis_line_color = INK_SOFT\np.xaxis.major_tick_line_color = None\np.yaxis.major_tick_line_color = None\np.xaxis.minor_tick_line_color = None\np.yaxis.minor_tick_line_color = None\n\n# Background — Imprint warm surface\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\n\n# Legend — bull/bear guide for viewers unfamiliar with candlestick conventions\np.legend.location = \"top_left\"\np.legend.label_text_font_size = \"28pt\"\np.legend.label_text_color = INK_SOFT\np.legend.background_fill_color = ELEVATED_BG\np.legend.border_line_color = INK_SOFT\np.legend.margin = 20\np.legend.padding = 16\np.legend.spacing = 10\n\n# Save HTML (interactive with hover tooltips)\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot with headless Chrome — Selenium 4 / Selenium Manager\nW, H = 3200, 1800\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.execute_cdp_cmd(\n    \"Emulation.setDeviceMetricsOverride\", {\"width\": W, \"height\": H, \"deviceScaleFactor\": 1, \"mobile\": False}\n)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}