{"spec_id":"drawdown-basic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\ndrawdown-basic: Drawdown Chart\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 87/100 | Updated: 2026-05-23\n\"\"\"\n\nimport base64\nimport os\nimport sys\nimport time\nfrom pathlib import Path\n\n\n# Remove the script's own directory from sys.path so \"bokeh\" resolves to the\n# installed package, not this file.\n_this_dir = str(Path(__file__).parent.resolve())\nsys.path = [p for p in sys.path if os.path.abspath(p) != _this_dir and p != \"\"]\n\nimport numpy as np\nimport pandas as pd\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, HoverTool, Label, Span\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# Imprint palette with semantic override: loss/drawdown → red\nDRAWDOWN_COLOR = \"#AE3030\"  # anyplot red (pos 3) — semantic: financial loss\nMAX_DD_COLOR = \"#4467A3\"  # anyplot sky blue (pos 4) — contrasting accent\nRECOVERY_COLOR = \"#009E73\"  # anyplot green (pos 1) — recovery / new highs\n\n# Data — simulate 3 years of daily portfolio returns\nnp.random.seed(42)\nn_days = 750\ndates = pd.date_range(\"2022-01-01\", periods=n_days, freq=\"B\")\n\nreturns = np.random.normal(0.0003, 0.015, n_days)\nreturns[200:250] = np.random.normal(-0.005, 0.025, 50)\nreturns[450:520] = np.random.normal(-0.008, 0.030, 70)\nreturns[600:630] = np.random.normal(-0.004, 0.020, 30)\n\nprices = 100 * np.exp(np.cumsum(returns))\nrunning_max = np.maximum.accumulate(prices)\ndrawdown = (prices - running_max) / running_max * 100\n\n# Find max drawdown\nmax_dd_idx = int(np.argmin(drawdown))\nmax_dd_value = drawdown[max_dd_idx]\nmax_dd_date = dates[max_dd_idx]\n\n# Max drawdown duration: from last peak before trough to the trough\npeak_idxs = np.where(drawdown[:max_dd_idx] >= -0.1)[0]\ndd_start_idx = int(peak_idxs[-1]) if len(peak_idxs) > 0 else 0\nmax_dd_duration = (dates[max_dd_idx] - dates[dd_start_idx]).days\n\n# Recovery time: from trough back to new high (drawdown ≥ 0)\nrec_idxs = np.where(drawdown[max_dd_idx:] >= -0.1)[0]\nif len(rec_idxs) > 0:\n    recovery_days = (dates[max_dd_idx + int(rec_idxs[0])] - dates[max_dd_idx]).days\n    recovery_str = f\"{recovery_days} days\"\nelse:\n    recovery_str = \"N/A\"\n\n# Find recovery points — transitions from negative drawdown back to zero (new highs)\nrecovery_dates = []\nfor i in range(1, len(drawdown)):\n    if drawdown[i - 1] < -0.5 and drawdown[i] >= -0.05:\n        recovery_dates.append(dates[i])\n\n# Plot\nsource = ColumnDataSource(data={\"date\": dates, \"drawdown\": drawdown, \"zero\": np.zeros(n_days)})\n\nW, H = 3200, 1800\np = figure(\n    width=W,\n    height=H,\n    title=\"drawdown-basic · python · bokeh · anyplot.ai\",\n    x_axis_label=\"Date\",\n    y_axis_label=\"Drawdown (%)\",\n    x_axis_type=\"datetime\",\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# HoverTool for interactivity\nhover = HoverTool(tooltips=[(\"Date\", \"@date{%F}\"), (\"Drawdown\", \"@drawdown{0.2f}%\")], formatters={\"@date\": \"datetime\"})\np.add_tools(hover)\n\n# Filled drawdown area\np.varea(x=\"date\", y1=\"zero\", y2=\"drawdown\", source=source, fill_color=DRAWDOWN_COLOR, fill_alpha=0.35)\n\n# Drawdown line\np.line(x=\"date\", y=\"drawdown\", source=source, line_color=DRAWDOWN_COLOR, line_width=3, legend_label=\"Drawdown\")\n\n# Zero baseline\np.add_layout(Span(location=0, dimension=\"width\", line_color=INK_SOFT, line_width=2))\n\n# Maximum drawdown marker\np.scatter(\n    x=[max_dd_date],\n    y=[max_dd_value],\n    size=20,\n    color=MAX_DD_COLOR,\n    marker=\"circle\",\n    legend_label=f\"Max DD: {max_dd_value:.1f}%\",\n)\n\n# Max drawdown annotation\np.add_layout(\n    Label(\n        x=max_dd_date,\n        y=max_dd_value,\n        text=f\"  {max_dd_value:.1f}%\",\n        text_font_size=\"30pt\",\n        text_color=MAX_DD_COLOR,\n        x_offset=12,\n        y_offset=-5,\n    )\n)\n\n# Stats block (data coords, upper area): max DD %, duration, recovery time\n_stats_x = dates[int(0.55 * n_days)]  # mid-right section, well before the right edge\n_stats = [\n    (f\"Max DD: {max_dd_value:.1f}%\", MAX_DD_COLOR),\n    (f\"Duration: {max_dd_duration} days\", INK),\n    (f\"Recovery: {recovery_str}\", RECOVERY_COLOR),\n]\nfor _i, (_text, _color) in enumerate(_stats):\n    p.add_layout(\n        Label(x=_stats_x, y=-4 - _i * 6, text=_text, text_font_size=\"28pt\", text_color=_color, text_align=\"left\")\n    )\n\n# Recovery (new high) markers\nif recovery_dates:\n    p.scatter(\n        x=recovery_dates,\n        y=[0.6] * len(recovery_dates),\n        size=24,\n        color=RECOVERY_COLOR,\n        marker=\"triangle\",\n        legend_label=\"New High\",\n    )\n\n# Font sizes\np.title.text_font_size = \"50pt\"\np.xaxis.axis_label_text_font_size = \"42pt\"\np.yaxis.axis_label_text_font_size = \"42pt\"\np.xaxis.major_label_text_font_size = \"34pt\"\np.yaxis.major_label_text_font_size = \"34pt\"\n\n# Legend\np.legend.location = \"bottom_left\"\np.legend.label_text_font_size = \"34pt\"\np.legend.background_fill_color = ELEVATED_BG\np.legend.border_line_color = INK_SOFT\np.legend.label_text_color = INK_SOFT\n\n# Theme-adaptive chrome\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = \"rgba(0,0,0,0)\"\np.title.text_color = INK\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\np.xaxis.major_label_text_color = INK_SOFT\np.yaxis.major_label_text_color = INK_SOFT\np.xaxis.axis_line_color = INK_SOFT\np.yaxis.axis_line_color = INK_SOFT\np.xaxis.major_tick_line_color = INK_SOFT\np.yaxis.major_tick_line_color = INK_SOFT\np.xgrid.grid_line_color = INK\np.ygrid.grid_line_color = INK\np.xgrid.grid_line_alpha = 0.10\np.ygrid.grid_line_alpha = 0.10\n\n# Save HTML artifact\noutput_file(f\"plot-{THEME}.html\", title=\"Drawdown Chart\")\nsave(p)\n\n# Screenshot with headless Chrome via CDP clip for exact pixel dimensions\n# (export_png uses snap chromedriver which is broken; save_screenshot clips at viewport)\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 + 200}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\nscreenshot = driver.execute_cdp_cmd(\n    \"Page.captureScreenshot\",\n    {\"format\": \"png\", \"captureBeyondViewport\": True, \"clip\": {\"x\": 0, \"y\": 0, \"width\": W, \"height\": H, \"scale\": 1}},\n)\ndriver.quit()\nimg_bytes = base64.b64decode(screenshot[\"data\"])\nwith open(f\"plot-{THEME}.png\", \"wb\") as f:\n    f.write(img_bytes)\n"}