{"spec_id":"drawdown-basic","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\ndrawdown-basic: Drawdown Chart\nLibrary: plotnine 0.15.4 | Python 3.13.13\nQuality: 83/100 | Updated: 2026-05-23\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    annotate,\n    element_line,\n    element_rect,\n    element_text,\n    geom_hline,\n    geom_line,\n    geom_point,\n    geom_ribbon,\n    ggplot,\n    labs,\n    scale_x_datetime,\n    scale_y_continuous,\n    theme,\n    theme_minimal,\n)\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_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\nDRAWDOWN_COLOR = \"#AE3030\"  # anyplot red — semantic: losses / drawdown\nRECOVERY_COLOR = \"#009E73\"  # anyplot green — semantic: recovery / new high\n# Higher alpha in dark mode so ribbon remains visible over near-black background\nRIBBON_ALPHA = 0.30 if THEME == \"light\" else 0.55\n\n# Data — synthetic portfolio with realistic drawdown patterns including one full recovery\nnp.random.seed(42)\nn_days = 500\ndates = pd.date_range(start=\"2022-01-01\", periods=n_days, freq=\"D\")\n\nreturns = np.random.normal(0.0008, 0.009, n_days)\n# First moderate drawdown (~15% peak-to-trough)\nreturns[40:80] = np.random.normal(-0.004, 0.010, 40)\n# Strong recovery to new all-time high — ensures at least one full recovery cycle\nreturns[80:160] = np.random.normal(0.005, 0.008, 80)\n# Second major drawdown — becomes the maximum drawdown\nreturns[180:250] = np.random.normal(-0.007, 0.015, 70)\n# Partial recovery — does not reach new ATH\nreturns[250:350] = np.random.normal(0.002, 0.012, 100)\n# Secondary dip\nreturns[350:420] = np.random.normal(-0.003, 0.012, 70)\n# Slow tail\nreturns[420:500] = np.random.normal(0.001, 0.010, 80)\n\nprice = 100 * np.cumprod(1 + returns)\n\ndf = pd.DataFrame({\"date\": dates, \"price\": price})\ndf[\"running_max\"] = df[\"price\"].cummax()\ndf[\"drawdown\"] = (df[\"price\"] - df[\"running_max\"]) / df[\"running_max\"] * 100\ndf[\"zero\"] = 0.0\n\n# Maximum drawdown statistics\nmax_dd_idx = df[\"drawdown\"].idxmin()\nmax_dd_value = df.loc[max_dd_idx, \"drawdown\"]\nmax_dd_date = df.loc[max_dd_idx, \"date\"]\nmax_drawdown = df[\"drawdown\"].min()\n\n# Max drawdown duration\ndf[\"in_drawdown\"] = df[\"drawdown\"] < -0.5\ndrawdown_groups = (df[\"in_drawdown\"] != df[\"in_drawdown\"].shift()).cumsum()\ndrawdown_durations = df[df[\"in_drawdown\"]].groupby(drawdown_groups).size()\nmax_duration = int(drawdown_durations.max()) if len(drawdown_durations) > 0 else 0\n\n# Recovery points: where drawdown transitions back to 0 (new all-time highs)\nprev_drawdown = df[\"drawdown\"].shift(1, fill_value=0.0)\nrecovery_mask = (df[\"drawdown\"] >= -0.01) & (prev_drawdown < -1.0)\nrecovery_df = df[recovery_mask].copy()\n\n# Recovery time: days from first drawdown entry to first complete recovery\nrecovery_time = None\ndrawdown_start_idx = None\nfor i in range(len(df)):\n    if df[\"drawdown\"].iloc[i] < -1.0 and drawdown_start_idx is None:\n        drawdown_start_idx = i\n    elif drawdown_start_idx is not None and df[\"drawdown\"].iloc[i] >= -0.01:\n        recovery_time = (df[\"date\"].iloc[i] - df[\"date\"].iloc[drawdown_start_idx]).days\n        break\n\n# Single-row DataFrame for max drawdown marker\nmax_dd_df = df.iloc[[max_dd_idx]].copy()\n\n# Caption with all three spec-required statistics\nif recovery_time is not None:\n    stats_label = (\n        f\"Max Drawdown: {max_drawdown:.1f}%  |  \"\n        f\"Max Duration: {max_duration} days  |  \"\n        f\"Recovery Time: {recovery_time} days\"\n    )\nelse:\n    stats_label = f\"Max Drawdown: {max_drawdown:.1f}%  |  Max Duration: {max_duration} days\"\n\n# Dynamic y-axis range to fit the data\ny_min = int(np.floor(max_drawdown / 5) * 5) - 5\ny_breaks = list(range(y_min, 5, 5))\n\n# Plot\nplot = (\n    ggplot(df, aes(x=\"date\", y=\"drawdown\"))\n    + geom_ribbon(aes(ymin=\"drawdown\", ymax=\"zero\"), fill=DRAWDOWN_COLOR, alpha=RIBBON_ALPHA)\n    + geom_line(color=DRAWDOWN_COLOR, size=1.0)\n    + geom_hline(yintercept=0, linetype=\"dashed\", color=INK_SOFT, size=0.7)\n    + geom_point(\n        aes(x=\"date\", y=\"drawdown\"), data=max_dd_df, color=PAGE_BG, fill=DRAWDOWN_COLOR, size=6, shape=\"o\", stroke=1.5\n    )\n    + annotate(\n        geom=\"text\",\n        x=max_dd_date + pd.Timedelta(days=22),\n        y=max_dd_value + 4,\n        label=f\"Max Drawdown: {max_drawdown:.1f}%\",\n        size=9,\n        color=INK,\n        ha=\"left\",\n    )\n    + labs(x=\"Date\", y=\"Drawdown (%)\", title=\"drawdown-basic · python · plotnine · anyplot.ai\", caption=stats_label)\n    + scale_x_datetime(date_breaks=\"3 months\", date_labels=\"%b %Y\")\n    + scale_y_continuous(breaks=y_breaks)\n    + theme_minimal()\n    + theme(\n        figure_size=(8, 4.5),\n        text=element_text(size=7),\n        axis_title=element_text(size=10, color=INK),\n        axis_text=element_text(size=8, color=INK_SOFT),\n        axis_text_x=element_text(angle=45, ha=\"right\", color=INK_SOFT),\n        plot_title=element_text(size=12, color=INK),\n        plot_caption=element_text(size=7, color=INK_MUTED),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n        panel_grid_major=element_line(color=INK, size=0.3, alpha=0.10),\n        panel_grid_minor=element_line(color=INK, size=0.2, alpha=0.05),\n        axis_line=element_line(color=INK_SOFT),\n    )\n)\n\n# Mark recovery points (new all-time highs after drawdown) with green diamonds\nif len(recovery_df) > 0:\n    plot = plot + geom_point(\n        aes(x=\"date\", y=\"zero\"), data=recovery_df, color=PAGE_BG, fill=RECOVERY_COLOR, size=5, shape=\"D\", stroke=1.0\n    )\n\n# Save\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=8, height=4.5, units=\"in\")\n"}