Domain: Financial & Accounting

You have a CSV of Tesla stock data with columns:
Date,Open,High,Low,Close,Volume,Daily_Return,Price_Range,Price_Change,Price_Change_Percent,MA_7,MA_30,MA_90,Volatility_7d,Year,Month,Day_of_Week,Quarter
Assume no NaN and data gaps.

Backtest the 4 strategies (S1-S4) on my Tesla CSV from 2011 onward, using monthly sampling = first trading day of each month, trade price = Close, fractional shares allowed, no fees/taxes.
Bond rule: sold amount goes into a bond bucket that compounds at 8% annually. The entire bond bucket compounds once each time a new calendar year begins during the simulation.

Strategies:
S1 — Fixed monthly investing (Buy & Hold)
Every month, on the first available trading day, you invest ₹10,000 into Tesla at the day's closing price.

S2 — Fixed monthly investing + take-profit into bonds
Every month, you invest ₹10,000 into Tesla (same as S1).
Additionally, if Tesla's closing price is above its short-term trend (specifically, more than 10% above the 7-day moving average), you sell ₹10,000 worth of Tesla that month (if you have enough holdings).
The money you sell is not kept as cash — it is moved into an 8% annual compound bond bucket, and remains there.

S3 — “Buy more on dips” ladder (No selling)
You invest only when price is weak, using a simple ladder with mutual exclusion. When more than one condition is met, use the highest applicable investment amount (for example, the MA_90 tier beats MA_30, which beats MA_7):
If Tesla is trading below its 90 day moving average, you invest ₹30,000 that month.
If it's below the 30 day moving average (but not the 90 day), you invest ₹15,000.
If it's below the 7 day moving average (but not the 30 day or 90-day), you invest ₹7,500.
You never sell in this strategy.

S4 — “Buy more on dips” ladder + take-profit into bonds
You follow the same monthly “buy more on dips” ladder as S3.
In addition, if Tesla becomes overextended upward (closing price is more than 10% above the 7-day moving average), you sell ₹10,000 worth of Tesla that month (limited by what you hold).
The amount sold is moved into the 8% annual compound bond bucket.

Compute these risk-adjusted metrics from the monthly equity curve:
sortino_rf8: A risk-adjusted performance measure that tells how efficiently a strategy generates returns above a benchmark “safe” rate of 8% per year, while only penalizing downside risk (months where performance falls short of that benchmark). Compute Sortino using monthly equity returns with rf_monthly = (1+0.08)**(1/12) - 1, and report the annualized Sortino.
ulcer_index: A drawdown pain measure that captures how deep and how long a portfolio stays below its previous peak over time.
calmar: A return vs crash measure that compares a strategy’s long-term growth to its worst peak-to-trough decline (maximum drawdown).

Pick best strategy using:
highest sortino_rf8
tie-break: lowest ulcer_index
tie-break: highest calmar

REQUIRED OUTPUT
Return only this Python dictionary (no extra text):
{
  "best_strategy": "strategy",
  "evidence": {
    "sortino_rf8": {"S1": x, "S2": y, "S3": z, "S4": w},
    "ulcer_index": {"S1": x, "S2": y, "S3": z, "S4": w},
    "calmar": {"S1": x, "S2": y, "S3": z, "S4": w}
  }
}
The values of each x, y, z, and w are rounded to 3 places.

Grading rubric:
criterion: The output is one Python dictionary with no extra surrounding text.
weight: 5
criterion: The output dict has exactly these top-level keys: 'best_strategy', 'evidence'
weight: 5
criterion: The 'best_strategy' key is equal to "S4"
weight: 10
criterion: The 'evidence' key is a dict containing exactly these keys: 'sortino_rf8', 'ulcer_index', 'calmar'
weight: 5
criterion: In 'evidence' sub dicts, each metric dict ('sortino_rf8', 'ulcer_index', 'calmar') contains exactly these keys: S1, S2, S3, S4
weight: 5
criterion: For 'sortino_rf8', 'S1' = 2.567, 'S2' = 2.567, 'S3' = 2.723, 'S4' = 2.723
weight: 15
criterion: For 'ulcer_index', 'S1' = 23.607, 'S2' = 23.504, 'S3' = 23.171, 'S4' = 23.110
weight: 15
criterion: For 'calmar', 'S1' = 1.145, 'S2' = 1.142, 'S3' = 1.151, 'S4' = 1.149
weight: 15

Golden answer:
```python
import pandas as pd
import numpy as np


CSV_PATH = "tesla_stock_data_2010_2025.csv"


def bond_accrue_annual(bond: float, prev_date, curr_date, annual_rate: float = 0.08) -> float:
    """Compound the bond bucket when the simulation crosses into a new calendar year."""
    if prev_date is None:
        return bond
    y0, y1 = prev_date.year, curr_date.year
    if y1 > y0:
        bond *= (1 + annual_rate) ** (y1 - y0)
    return bond


def max_drawdown(equity: np.ndarray):
    """Return the worst peak-to-trough decline (scalar) and the full drawdown series."""
    peak = np.maximum.accumulate(equity)
    dd = (equity - peak) / peak
    mdd = float(dd.min())
    return mdd, dd


def cagr_from_equity(equity: np.ndarray, dates: pd.Series) -> float:
    """Annualised growth rate from first to last equity value over the elapsed years."""
    if equity[0] <= 0:
        return float("nan")
    years = (dates.iloc[-1] - dates.iloc[0]).days / 365.25
    if years <= 0:
        return float("nan")
    return float((equity[-1] / equity[0]) ** (1 / years) - 1)


def ulcer_index(drawdowns: np.ndarray) -> float:
    """RMS of the percentage-drawdown series — penalises deep, prolonged underwater periods."""
    dd_pct = drawdowns * 100.0
    return float(np.sqrt(np.mean(dd_pct ** 2)))


def downside_deviation(returns: np.ndarray, mar: float) -> float:
    """RMS of monthly shortfalls below the minimum acceptable return; upside months count as 0."""
    downside = np.minimum(0.0, returns - mar)
    return float(np.sqrt(np.mean(downside ** 2)))


def sortino_ratio(returns: np.ndarray, rf_monthly: float) -> float:
    """Annualised Sortino: mean monthly excess return over rf divided by downside deviation."""
    excess_mean = float(np.mean(returns - rf_monthly))
    dd = downside_deviation(returns, mar=rf_monthly)
    if dd == 0 or not np.isfinite(dd):
        return float("nan")
    return float(excess_mean / dd * np.sqrt(12))


def buy_amount_s3(close_price: float, ma7: float, ma30: float, ma90: float) -> float:
    """Dip-buying ladder: strongest dip wins; np.isfinite skips MAs not yet available."""
    if np.isfinite(ma90) and close_price < ma90:
        return 30000.0
    if np.isfinite(ma30) and close_price < ma30:
        return 15000.0
    if np.isfinite(ma7) and close_price < ma7:
        return 7500.0
    return 0.0


def monthly_first_trading_day(df: pd.DataFrame) -> pd.DataFrame:
    """Keep only the first trading day of each calendar month."""
    df = df.copy()
    df["YearMonth"] = df["Date"].dt.to_period("M")
    return df.groupby("YearMonth", as_index=False).first().sort_values("Date").reset_index(drop=True)


def run():
    df = pd.read_csv(CSV_PATH)
    df["Date"] = pd.to_datetime(df["Date"])
    df = df.sort_values("Date").reset_index(drop=True)

    df = df[df["Date"] >= pd.Timestamp("2011-01-01")].copy()

    m = monthly_first_trading_day(df)
    dates = m["Date"]

    # geometric monthly equivalent of the 8% annual hurdle rate
    rf_monthly = (1 + 0.08) ** (1 / 12) - 1

    def strat_s1():
        shares = 0.0
        equity = []
        for _, r in m.iterrows():
            p = float(r["Close"])
            shares += 10000.0 / p
            equity.append(shares * p)
        return np.array(equity, dtype=float)

    def strat_s2():
        shares = 0.0
        bond = 0.0
        equity = []
        prev = None
        for _, r in m.iterrows():
            d = r["Date"].date()
            bond = bond_accrue_annual(bond, prev, d, 0.08)
            prev = d

            p = float(r["Close"])
            ma7 = float(r["MA_7"]) if np.isfinite(r["MA_7"]) else np.nan

            shares += 10000.0 / p

            # take-profit: sell up to ₹10k into the bond when price is overextended
            if np.isfinite(ma7) and p > 1.10 * ma7 and shares > 0:
                value = min(10000.0, shares * p)
                shares -= value / p
                bond += value

            equity.append(shares * p + bond)
        return np.array(equity, dtype=float)

    def strat_s3():
        shares = 0.0
        equity = []
        for _, r in m.iterrows():
            p = float(r["Close"])
            ma7  = float(r["MA_7"])  if np.isfinite(r["MA_7"])  else np.nan
            ma30 = float(r["MA_30"]) if np.isfinite(r["MA_30"]) else np.nan
            ma90 = float(r["MA_90"]) if np.isfinite(r["MA_90"]) else np.nan

            buy = buy_amount_s3(p, ma7, ma30, ma90)
            if buy > 0:
                shares += buy / p

            equity.append(shares * p)
        return np.array(equity, dtype=float)

    def strat_s4():
        shares = 0.0
        bond = 0.0
        equity = []
        prev = None
        for _, r in m.iterrows():
            d = r["Date"].date()
            bond = bond_accrue_annual(bond, prev, d, 0.08)
            prev = d

            p = float(r["Close"])
            ma7  = float(r["MA_7"])  if np.isfinite(r["MA_7"])  else np.nan
            ma30 = float(r["MA_30"]) if np.isfinite(r["MA_30"]) else np.nan
            ma90 = float(r["MA_90"]) if np.isfinite(r["MA_90"]) else np.nan

            buy = buy_amount_s3(p, ma7, ma30, ma90)
            if buy > 0:
                shares += buy / p

            # same take-profit rule as S2
            if np.isfinite(ma7) and p > 1.10 * ma7 and shares > 0:
                value = min(10000.0, shares * p)
                shares -= value / p
                bond += value

            equity.append(shares * p + bond)
        return np.array(equity, dtype=float)

    eq = {
        "S1": strat_s1(),
        "S2": strat_s2(),
        "S3": strat_s3(),
        "S4": strat_s4(),
    }

    evidence = {"sortino_rf8": {}, "ulcer_index": {}, "calmar": {}}

    for k, equity in eq.items():
        rets = np.diff(equity) / equity[:-1]  # month-over-month returns
        mdd, dd_series = max_drawdown(equity)
        cagr = cagr_from_equity(equity, dates)
        calmar = cagr / abs(mdd) if (np.isfinite(cagr) and mdd < 0) else float("nan")

        evidence["sortino_rf8"][k] = round(sortino_ratio(rets, rf_monthly), 3)
        evidence["ulcer_index"][k] = round(ulcer_index(dd_series), 3)
        evidence["calmar"][k]      = round(float(calmar), 3)

    # negation flips "higher is better" metrics so a single ascending sort handles all three
    keys = list(eq.keys())
    best = sorted(
        keys,
        key=lambda s: (
            -evidence["sortino_rf8"][s],
             evidence["ulcer_index"][s],
            -evidence["calmar"][s],
        ),
    )[0]

    out = {
        "best_strategy": best,
        "evidence": evidence,
    }

    print(out)


if __name__ == "__main__":
    run()

```
**Explanation:**

The CSV is filtered to 2011 onward and downsampled to one row per calendar month (the first trading day), which becomes the single trade point for all strategies.

Each strategy walks the monthly rows and builds an equity curve, the portfolio's total value at each step. S1 is plain dollar-cost averaging: ₹10,000 buys fractional shares at the Close price every month, and equity is just accumulated shares marked to the current price. S2 adds a take profit rule on top: after the fixed buy, if the closing price is more than 10% above the 7 day MA, ₹10,000 worth of Tesla is liquidated into a bond bucket that compounds at 8% whenever a new calendar year begins. S3 replaces the fixed buy with a dip ladder, it invests ₹30,000 when below MA_90, ₹15,000 when below MA_30 (but not MA_90), or ₹7,500 when below MA_7 (but not MA_30/MA_90), and nothing when above all MAs, there is no bond and no selling. S4 combines S3's dip ladder with S2's take-profit rule, accumulating both Tesla shares and a compounding bond bucket.

Once all four equity curves are built, three risk adjusted metrics are computed from each. The Sortino ratio measures return above the 8% annual hurdle per unit of downside risk: the numerator is the mean monthly excess return above the geometric monthly equivalent of 8%, the denominator is the RMS of months that fall short of that hurdle, and the result is scaled by the square root of 12 to annualise. The Ulcer Index is the RMS of the percentage drawdown series, squaring the drawdowns means deep or prolonged underwater stretches dominate the score. The Calmar ratio is CAGR divided by the absolute maximum drawdown, rewarding strategies that grow fast relative to their worst historical crash.

Finally, strategies are ranked by Sortino descending, then Ulcer Index ascending, then Calmar descending. Negating the two "higher is better" metrics lets a single `sorted()` call handle all three criteria in one pass.

**GTFA:** {'best_strategy': 'S4', 'evidence': {'sortino_rf8': {'S1': 2.567, 'S2': 2.567, 'S3': 2.723, 'S4': 2.723}, 'ulcer_index': {'S1': 23.607, 'S2': 23.504, 'S3': 23.171, 'S4': 23.11}, 'calmar': {'S1': 1.145, 'S2': 1.142, 'S3': 1.151, 'S4': 1.149}}}