{"spec_id":"candlestick-basic","library":"matplotlib","language":"python","code":"\"\"\" anyplot.ai\ncandlestick-basic: Basic Candlestick Chart\nLibrary: matplotlib 3.10.9 | Python 3.13.13\nQuality: 91/100 | Updated: 2026-05-30\n\"\"\"\n\nimport os\n\nimport matplotlib.dates as mdates\nimport matplotlib.patches as mpatches\nimport matplotlib.pyplot as plt\nimport matplotlib.ticker as mticker\nimport numpy as np\nimport pandas as pd\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint palette — semantic exception: finance profit/loss → green/red\nCOLOR_UP = \"#009E73\"  # Imprint position 1 (brand green) — bullish / gain\nCOLOR_DOWN = \"#AE3030\"  # Imprint position 5 (matte red) — bearish / loss\n\n# Data\nnp.random.seed(42)\nn_days = 30\ndates = pd.bdate_range(start=\"2024-01-02\", periods=n_days)\n\n# Random walk prices starting at $150\nreturns = np.random.randn(n_days) * 0.02\nprice_series = 150 * np.exp(np.cumsum(returns))\n\n# Generate OHLC from base prices\nopen_prices = price_series * (1 + np.random.uniform(-0.005, 0.005, n_days))\nclose_prices = price_series * (1 + np.random.uniform(-0.015, 0.015, n_days))\nintraday_ranges = price_series * np.random.uniform(0.01, 0.03, n_days)\nlow_prices = np.minimum(open_prices, close_prices) - np.random.uniform(0, 0.5, n_days) * intraday_ranges\nhigh_prices = np.maximum(open_prices, close_prices) + np.random.uniform(0, 0.5, n_days) * intraday_ranges\n\ndf = pd.DataFrame({\"date\": dates, \"open\": open_prices, \"high\": high_prices, \"low\": low_prices, \"close\": close_prices})\n\n# Volume data — correlated with daily price range (higher volatility → more trading)\ndaily_range_pct = (df[\"high\"] - df[\"low\"]) / price_series\ndf[\"volume\"] = (1_000_000 * (1 + daily_range_pct * 10) * np.random.uniform(0.7, 1.3, n_days)).astype(int)\n\n# 5-day simple moving average for trend context\ndf[\"sma5\"] = df[\"close\"].rolling(window=5).mean()\n\n# Pre-compute price range for proportional minimum body height\ny_min_data, y_max_data = df[\"low\"].min(), df[\"high\"].max()\nmin_body = (y_max_data - y_min_data) * 0.004  # scales with axis range; avoids invisible doji bodies\n\n# Plot with gridspec: main candlestick panel (75%) + volume panel (25%)\nfig = plt.figure(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)\ngs = fig.add_gridspec(2, 1, height_ratios=[3, 1], hspace=0.05)\nax = fig.add_subplot(gs[0])\nax_vol = fig.add_subplot(gs[1], sharex=ax)\n\nax.set_facecolor(PAGE_BG)\nax_vol.set_facecolor(PAGE_BG)\n\nbullish = df[\"close\"] >= df[\"open\"]\ncolors = np.where(bullish, COLOR_UP, COLOR_DOWN)\ndate_nums = mdates.date2num(df[\"date\"])\nwidth = 0.6\n\n# Wicks — thin lines for high-low range (behind bodies)\nax.vlines(date_nums, df[\"low\"], df[\"high\"], colors=colors, linewidth=1.5, zorder=1)\n\n# Bodies — bars for open-close range (in front of wicks)\nbody_bottoms = np.where(bullish, df[\"open\"], df[\"close\"])\nbody_heights = np.abs(df[\"close\"] - df[\"open\"])\nbody_heights = np.where(body_heights < min_body, min_body, body_heights)\nax.bar(\n    date_nums, body_heights, bottom=body_bottoms, width=width, color=colors, edgecolor=colors, linewidth=0.8, zorder=2\n)\n\n# 5-day SMA — dashed to distinguish from structural chrome\nsma_mask = df[\"sma5\"].notna()\nax.plot(date_nums[sma_mask], df[\"sma5\"][sma_mask], color=INK_MUTED, linewidth=2.0, linestyle=\"--\", alpha=0.9, zorder=3)\n\n# Annotate largest single-day price drop\ndaily_change = df[\"close\"] - df[\"open\"]\nbiggest_drop_idx = daily_change.idxmin()\ndrop_val = daily_change[biggest_drop_idx]\nax.annotate(\n    f\"Largest drop\\n-${abs(drop_val):.2f}\",\n    xy=(date_nums[biggest_drop_idx], df[\"low\"].iloc[biggest_drop_idx]),\n    xytext=(0, -28),\n    textcoords=\"offset points\",\n    fontsize=8,\n    fontweight=\"medium\",\n    color=COLOR_DOWN,\n    ha=\"center\",\n    va=\"top\",\n    arrowprops={\"arrowstyle\": \"->\", \"color\": COLOR_DOWN, \"lw\": 1.2},\n    zorder=4,\n)\n\n# Volume bars in lower panel — color-coded to match candle direction\nax_vol.bar(date_nums, df[\"volume\"], width=width, color=colors, alpha=0.7, zorder=2)\n\n# Date formatting on lower panel (x-axis labels hidden on upper via sharex)\nax_vol.xaxis.set_major_locator(mdates.WeekdayLocator(byweekday=mdates.MO))\nax_vol.xaxis.set_major_formatter(mdates.DateFormatter(\"%b %d\"))\nax_vol.tick_params(which=\"minor\", length=0)  # suppress daily minor tick clutter\nax_vol.tick_params(axis=\"x\", rotation=45, labelsize=8, colors=INK_SOFT, labelcolor=INK_SOFT)\nax_vol.tick_params(axis=\"y\", labelsize=7, colors=INK_SOFT, labelcolor=INK_SOFT)\nplt.setp(ax.get_xticklabels(), visible=False)\n\n# Style — main panel\ntitle = \"candlestick-basic · python · matplotlib · anyplot.ai\"\ntitle_fontsize = max(8, round(12 * 67 / len(title))) if len(title) > 67 else 12\n\nax.set_ylabel(\"Price (USD)\", fontsize=10, color=INK)\nax.set_title(title, fontsize=title_fontsize, fontweight=\"medium\", color=INK, pad=10)\nax.tick_params(axis=\"both\", labelsize=8, colors=INK_SOFT, labelcolor=INK_SOFT)\nax.tick_params(which=\"minor\", length=0)  # suppress daily minor tick clutter\n\nax.spines[\"top\"].set_visible(False)\nax.spines[\"right\"].set_visible(False)\nax.spines[\"left\"].set_linewidth(0.6)\nax.spines[\"left\"].set_color(INK_SOFT)\nax.spines[\"bottom\"].set_linewidth(0.6)\nax.spines[\"bottom\"].set_color(INK_SOFT)\n\nax.yaxis.set_major_formatter(mticker.FormatStrFormatter(\"$%.0f\"))\nax.yaxis.grid(True, alpha=0.15, linewidth=0.6, color=INK)\nax.set_axisbelow(True)\n\n# Style — volume panel\nax_vol.set_ylabel(\"Volume\", fontsize=9, color=INK)\nax_vol.set_xlabel(\"Date\", fontsize=10, color=INK)\nax_vol.spines[\"top\"].set_visible(False)\nax_vol.spines[\"right\"].set_visible(False)\nax_vol.spines[\"left\"].set_linewidth(0.6)\nax_vol.spines[\"left\"].set_color(INK_SOFT)\nax_vol.spines[\"bottom\"].set_linewidth(0.6)\nax_vol.spines[\"bottom\"].set_color(INK_SOFT)\nax_vol.yaxis.set_major_formatter(mticker.FuncFormatter(lambda x, _: f\"{x / 1e6:.1f}M\"))\nax_vol.yaxis.grid(True, alpha=0.15, linewidth=0.6, color=INK)\nax_vol.set_axisbelow(True)\n\n# Legend on main panel\nlegend_handles = [\n    mpatches.Patch(color=COLOR_UP, label=\"Bullish (Close ≥ Open)\"),\n    mpatches.Patch(color=COLOR_DOWN, label=\"Bearish (Close < Open)\"),\n    plt.Line2D([0], [0], color=INK_MUTED, linewidth=2.0, linestyle=\"--\", alpha=0.9, label=\"5-day SMA\"),\n]\nleg = ax.legend(\n    handles=legend_handles, fontsize=8, loc=\"upper right\", framealpha=0.9, edgecolor=INK_SOFT, facecolor=ELEVATED_BG\n)\nfor t in leg.get_texts():\n    t.set_color(INK_SOFT)\n\n# Axis limits with padding\ny_pad = (y_max_data - y_min_data) * 0.18\nax.set_ylim(y_min_data - y_pad, y_max_data + y_pad)\nx_min = mdates.date2num(df[\"date\"].min())\nx_max = mdates.date2num(df[\"date\"].max())\nax.set_xlim(x_min - 1, x_max + 1)\n\nfig.subplots_adjust(left=0.10, right=0.97, top=0.92, bottom=0.22)\n\n# Save\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}