{"spec_id":"drawdown-basic","library":"altair","language":"python","code":"\"\"\" anyplot.ai\ndrawdown-basic: Drawdown Chart\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 91/100 | Updated: 2026-05-23\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\n\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\nDD_COLOR = \"#AE3030\"  # Imprint palette pos 3 — semantic: loss/drawdown\nREC_COLOR = \"#009E73\"  # Imprint palette pos 1 — semantic: recovery/new high\n\n# Data — simulated asset price over ~2 years with drawdowns and recoveries\nnp.random.seed(42)\ndates = pd.date_range(\"2022-01-01\", periods=500, freq=\"D\")\nreturns = np.random.normal(0.001, 0.01, 500)\n\nreturns[0:30] = np.random.normal(0.002, 0.008, 30)  # initial rally\nreturns[30:50] = np.random.normal(-0.012, 0.012, 20)  # correction ~20%\nreturns[50:90] = np.random.normal(0.008, 0.008, 40)  # recovery to new highs\nreturns[100:130] = np.random.normal(-0.015, 0.015, 30)  # deeper correction ~30%\nreturns[130:200] = np.random.normal(0.006, 0.009, 70)  # recovery to new highs\nreturns[220:270] = np.random.normal(-0.01, 0.012, 50)  # extended decline ~35%\nreturns[270:350] = np.random.normal(0.005, 0.008, 80)  # gradual recovery\nreturns[380:410] = np.random.normal(-0.008, 0.01, 30)  # late correction ~15%\nreturns[410:480] = np.random.normal(0.004, 0.007, 70)  # final recovery\n\nprice = 100 * np.cumprod(1 + returns)\nrunning_max = np.maximum.accumulate(price)\ndrawdown = (price - running_max) / running_max * 100\n\ndf = pd.DataFrame({\"date\": dates, \"drawdown\": drawdown})\n\n# Max drawdown stats\nmax_dd_idx = df[\"drawdown\"].idxmin()\nmax_dd_date = df.loc[max_dd_idx, \"date\"]\nmax_dd_value = df.loc[max_dd_idx, \"drawdown\"]\nmax_drawdown_pct = abs(max_dd_value)\n\n# Drawdown duration (peak → trough)\ndd_start_candidates = df.loc[:max_dd_idx][df.loc[:max_dd_idx, \"drawdown\"] == 0]\ndd_start_date = dd_start_candidates.iloc[-1][\"date\"] if not dd_start_candidates.empty else dates[0]\ndd_duration_days = (max_dd_date - dd_start_date).days\n\n# Recovery (trough → next new high)\npost_max = df[df.index > max_dd_idx]\nrecovery_row = post_max[post_max[\"drawdown\"] >= 0]\nrecovery_days = int((recovery_row.iloc[0][\"date\"] - max_dd_date).days) if not recovery_row.empty else None\nrecovery_str = f\"{recovery_days}d\" if recovery_days else \"ongoing\"\n\n# Recovery points: first new high after any drawdown deeper than -5%\ndf[\"prev_dd\"] = df[\"drawdown\"].shift(1)\ndf[\"is_new_high\"] = (df[\"drawdown\"] >= 0) & (df[\"prev_dd\"] < 0)\nrec_indices = []\nlast_low = 0.0\nfor idx, row in df.iterrows():\n    if row[\"drawdown\"] < last_low:\n        last_low = row[\"drawdown\"]\n    if row[\"is_new_high\"] and last_low < -5:\n        rec_indices.append(idx)\n        last_low = 0.0\n\nrecovery_points = df.loc[rec_indices, [\"date\", \"drawdown\"]].copy().reset_index(drop=True)\nrecovery_points[\"series\"] = \"New High\"\n\nmax_dd_df = pd.DataFrame(\n    {\"date\": [max_dd_date], \"drawdown\": [max_dd_value], \"label\": [f\"Max DD: {max_drawdown_pct:.1f}%\"]}\n)\n\ntitle_str = \"drawdown-basic · python · altair · anyplot.ai\"\nsubtitle_str = f\"Max Drawdown: {max_drawdown_pct:.1f}%  ·  Duration: {dd_duration_days}d  ·  Recovery: {recovery_str}\"\n\n# Plot layers\narea = (\n    alt.Chart(df)\n    .mark_area(opacity=0.45, color=DD_COLOR)\n    .encode(\n        x=alt.X(\"date:T\", title=\"Date\", axis=alt.Axis(format=\"%b %Y\")),\n        y=alt.Y(\"drawdown:Q\", title=\"Drawdown (%)\", scale=alt.Scale(domain=[df[\"drawdown\"].min() * 1.15, 5])),\n    )\n)\n\ndd_line = alt.Chart(df).mark_line(color=DD_COLOR, strokeWidth=2).encode(x=\"date:T\", y=\"drawdown:Q\")\n\nzero_rule = (\n    alt.Chart(pd.DataFrame({\"y\": [0]}))\n    .mark_rule(strokeDash=[5, 4], strokeWidth=1.5)\n    .encode(y=\"y:Q\", color=alt.value(INK_SOFT))\n)\n\nmax_dd_point = (\n    alt.Chart(max_dd_df)\n    .mark_point(size=250, color=\"#C475FD\", filled=True, stroke=INK, strokeWidth=1.5)\n    .encode(x=\"date:T\", y=\"drawdown:Q\")\n)\n\nmax_dd_label = (\n    alt.Chart(max_dd_df)\n    .mark_text(align=\"left\", dx=12, dy=-8, fontSize=11, fontWeight=\"bold\")\n    .encode(x=\"date:T\", y=\"drawdown:Q\", text=\"label:N\", color=alt.value(INK))\n)\n\nlayers = [area, dd_line, zero_rule, max_dd_point, max_dd_label]\n\nif len(recovery_points) > 0:\n    recovery_markers = (\n        alt.Chart(recovery_points)\n        .mark_point(shape=\"triangle-up\", filled=True, size=150, stroke=PAGE_BG, strokeWidth=1)\n        .encode(\n            x=\"date:T\",\n            y=\"drawdown:Q\",\n            color=alt.Color(\n                \"series:N\", scale=alt.Scale(range=[REC_COLOR]), legend=alt.Legend(title=\"\", orient=\"top-right\")\n            ),\n            tooltip=[alt.Tooltip(\"date:T\", title=\"New High\", format=\"%Y-%m-%d\")],\n        )\n    )\n    layers.append(recovery_markers)\n\nchart = (\n    alt.layer(*layers)\n    .properties(\n        width=620,\n        height=320,\n        background=PAGE_BG,\n        title=alt.Title(\n            title_str,\n            fontSize=16,\n            color=INK,\n            anchor=\"start\",\n            subtitle=subtitle_str,\n            subtitleFontSize=12,\n            subtitleColor=INK_SOFT,\n        ),\n    )\n    .configure_view(fill=PAGE_BG, stroke=None, continuousWidth=620, continuousHeight=320)\n    .configure_axis(\n        labelFontSize=10,\n        titleFontSize=12,\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        gridColor=INK,\n        gridOpacity=0.10,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n    )\n    .configure_axisX(grid=False)\n    .configure_title(color=INK, fontSize=16, subtitleFontSize=12, subtitleColor=INK_SOFT)\n    .configure_legend(\n        fillColor=ELEVATED_BG,\n        strokeColor=INK_SOFT,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        labelFontSize=10,\n        titleFontSize=10,\n    )\n)\n\n# Save PNG and pad to exact canvas target\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\nTW, TH = 3200, 1800\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. \"\n        f\"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\nchart.save(f\"plot-{THEME}.html\")\n"}