{"spec_id":"drawdown-basic","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\ndrawdown-basic: Drawdown Chart\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 83/100 | Updated: 2026-05-23\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nimport pygal\nfrom pygal.style import Style\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Data — 2 years of synthetic daily portfolio returns\nnp.random.seed(42)\ndates = pd.date_range(\"2022-01-01\", periods=500, freq=\"D\")\nreturns = np.random.normal(0.0003, 0.018, len(dates))\nprice = 100 * np.cumprod(1 + returns)\n\n# Drawdown: % decline from running maximum\nrunning_max = np.maximum.accumulate(price)\ndrawdown = (price - running_max) / running_max * 100\n\n# Max drawdown stats\nmax_dd_idx = int(np.argmin(drawdown))\nmax_dd_value = drawdown[max_dd_idx]\nmax_dd_date = dates[max_dd_idx].strftime(\"%Y-%m-%d\")\n\n# Duration: days from peak to trough\npeak_slice = price[: max_dd_idx + 1]\npeak_idx = int(np.where(peak_slice == peak_slice.max())[0][-1])\nduration_days = (dates[max_dd_idx] - dates[peak_idx]).days\n\n# Recovery time: days from trough to next new high\nrecovery_after = [i for i in range(max_dd_idx, len(drawdown)) if drawdown[i] >= -0.1]\nif recovery_after:\n    recovery_days = (dates[recovery_after[0]] - dates[max_dd_idx]).days\n    recovery_str = f\"{recovery_days}d\"\nelse:\n    recovery_str = \"Not recovered\"\n\n# Recovery indices: where drawdown crosses back to ~zero (new highs)\nrecovery_indices = [i for i in range(1, len(drawdown)) if drawdown[i - 1] < -0.5 and drawdown[i] >= -0.1]\n\n# Style — semantic colors: red = loss/drawdown, purple = peak loss marker, green = recovery\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    colors=(\"#AE3030\", \"#C475FD\", \"#009E73\", INK_MUTED),\n    title_font_size=66,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    value_font_size=36,\n    stroke_width=3,\n    opacity=\".65\",\n    opacity_hover=\".85\",\n)\n\nchart = pygal.Line(\n    width=3200,\n    height=1800,\n    title=\"drawdown-basic · python · pygal · anyplot.ai\",\n    x_title=\"Date\",\n    y_title=\"Drawdown (%)\",\n    style=custom_style,\n    show_dots=False,\n    stroke_style={\"width\": 4},\n    fill=False,\n    show_y_guides=True,\n    show_x_guides=False,\n    x_label_rotation=45,\n    legend_at_bottom=True,\n    truncate_legend=-1,\n    range=(min(drawdown) * 1.1, 5),\n)\n\n# X-axis labels — every ~60 days for readability\nx_labels = [d.strftime(\"%Y-%m\") if i % 60 == 0 else \"\" for i, d in enumerate(dates)]\nchart.x_labels = x_labels\n\n# Main drawdown series with fill — per-series fill keeps the area clean\nchart.add(f\"Drawdown (Max: {max_dd_value:.1f}% on {max_dd_date})\", list(drawdown), fill=True)\n\n# Max drawdown marker — prominent dot at the trough (no fill, just the dot)\nmax_marker = [None] * len(drawdown)\nmax_marker[max_dd_idx] = drawdown[max_dd_idx]\nchart.add(\n    f\"Max Drawdown: {max_dd_value:.1f}% | Duration: {duration_days}d | Recovery: {recovery_str}\",\n    max_marker,\n    show_dots=True,\n    dots_size=18,\n)\n\n# Recovery point markers — green dots at zero-crossings (no fill since values ≈ 0)\nif recovery_indices:\n    recovery_data = [None] * len(drawdown)\n    for idx in recovery_indices[:8]:\n        recovery_data[idx] = 0.0\n    chart.add(\"Recovery Points\", recovery_data, show_dots=True, dots_size=14)\n\n# Save (theme-suffixed — pipeline runs this script twice)\nchart.render_to_file(f\"plot-{THEME}.html\")\nchart.render_to_png(f\"plot-{THEME}.png\")\n"}